@opengeni/config 0.2.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/dist/index.js ADDED
@@ -0,0 +1,1728 @@
1
+ // src/index.ts
2
+ import {
3
+ BillingMode,
4
+ CAPABILITY_DESCRIPTORS,
5
+ Entitlements,
6
+ EntitlementsMode,
7
+ ProductAccessMode,
8
+ ReasoningEffort,
9
+ SandboxBackend,
10
+ StaticUsageLimits,
11
+ UsageLimitsMode
12
+ } from "@opengeni/contracts";
13
+ import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex/constants";
14
+ import { z } from "zod";
15
+ var envName = /^[A-Za-z_][A-Za-z0-9_]*$/;
16
+ var registryId = /^[A-Za-z0-9_-]+$/;
17
+ var EnvBoolean = z.preprocess((value) => {
18
+ if (typeof value !== "string") {
19
+ return value;
20
+ }
21
+ const normalized = value.trim().toLowerCase();
22
+ if (["true", "1", "yes", "y", "on"].includes(normalized)) {
23
+ return true;
24
+ }
25
+ if (["false", "0", "no", "n", "off"].includes(normalized)) {
26
+ return false;
27
+ }
28
+ return value;
29
+ }, z.boolean());
30
+ var sandboxPreparationProfiles = {
31
+ none: {
32
+ env: [],
33
+ hooks: []
34
+ },
35
+ azure: {
36
+ env: [
37
+ "ARM_CLIENT_ID",
38
+ "ARM_CLIENT_SECRET",
39
+ "ARM_TENANT_ID",
40
+ "ARM_SUBSCRIPTION_ID",
41
+ "AZURE_CLIENT_ID",
42
+ "AZURE_CLIENT_SECRET",
43
+ "AZURE_TENANT_ID",
44
+ "AZURE_SUBSCRIPTION_ID",
45
+ "AZURE_AUTHORITY_HOST"
46
+ ],
47
+ hooks: ["azure-cli-login"]
48
+ },
49
+ github: {
50
+ env: [
51
+ "GH_TOKEN",
52
+ "GITHUB_TOKEN",
53
+ "GIT_AUTHOR_NAME",
54
+ "GIT_AUTHOR_EMAIL",
55
+ "GIT_COMMITTER_NAME",
56
+ "GIT_COMMITTER_EMAIL"
57
+ ],
58
+ hooks: []
59
+ }
60
+ };
61
+ var AGENT_INSTRUCTIONS_CORE_PLACEHOLDER = "{{core}}";
62
+ var DEFAULT_AGENT_INSTRUCTIONS = [
63
+ "You are an OpenGeni workspace agent.",
64
+ "Follow the user's task and any enabled pack or skill instructions for the current role.",
65
+ "Work inside the sandbox workspace and use filesystem and shell tools when useful.",
66
+ "Repository resources are mounted under repos/<owner>/<repo>.",
67
+ "File resources are mounted under files/<file-id>/ unless the session specifies another mount path.",
68
+ "Attached files are mounted read-only; copy them before modifying.",
69
+ "Bundled skills are under .agents/ and can include infrastructure, marketing, or other role-specific guidance.",
70
+ "Use Checkov, Terraform, Azure CLI, GitHub CLI, and repository tools when relevant.",
71
+ "When the Azure sandbox preparation profile is enabled and service-principal variables are present, the sandbox is pre-authenticated with normal Azure CLI before work starts.",
72
+ "Treat code-changing work as GitOps work: create a focused branch/commit/PR when GitHub credentials are available; otherwise report exact commands and blockers.",
73
+ "Return concise, factual summaries with files changed, commands run, and remaining blockers.",
74
+ AGENT_INSTRUCTIONS_CORE_PLACEHOLDER
75
+ ].join(" ");
76
+ var SettingsSchema = z.object({
77
+ serviceName: z.string().default("opengeni"),
78
+ environment: z.string().default("local"),
79
+ deploymentRevision: z.string().default("dev"),
80
+ databaseUrl: z.string().default("postgres://opengeni:opengeni@127.0.0.1:5432/opengeni"),
81
+ // Step I (§7.8 runtime half). Dedicated Postgres schema for the EMBEDDED
82
+ // topology. Default "" → standalone: no search_path scoping, server default
83
+ // (`public`). When set (e.g. "opengeni"), the db handle + the managed-auth
84
+ // pool send `search_path = "<dbSchema>","opengeni_private","public"` so every
85
+ // query resolves into the dedicated schema with NO query rewrite (SPIKE-1 F1).
86
+ dbSchema: z.string().default(""),
87
+ // Step I (§7.7). RLS posture. "force" (default) = today's FORCE-RLS via the
88
+ // non-owner `opengeni_app` role. "scoped" = the embedded owner-role path (the
89
+ // GUC is still emitted defensively, so the query path is identical).
90
+ rlsStrategy: z.enum(["force", "scoped"]).default("force"),
91
+ natsUrl: z.string().default("nats://127.0.0.1:4222"),
92
+ temporalHost: z.string().default("127.0.0.1:7233"),
93
+ temporalNamespace: z.string().default("default"),
94
+ temporalTaskQueue: z.string().default("opengeni-runs-ts"),
95
+ startupDependencyRetryAttempts: z.coerce.number().int().positive().default(30),
96
+ startupDependencyRetryInitialDelayMs: z.coerce.number().int().positive().default(1e3),
97
+ startupDependencyRetryMaxDelayMs: z.coerce.number().int().positive().default(5e3),
98
+ observabilityStructuredLogs: EnvBoolean.default(false),
99
+ observabilityMetricsEnabled: EnvBoolean.default(true),
100
+ observabilityOtlpEndpoint: z.string().url().optional(),
101
+ observabilityOtlpHeaders: z.string().default(""),
102
+ publicBaseUrl: z.string().url().optional(),
103
+ // Base URL for the bring-your-own-compute agent release assets the get.<domain>
104
+ // install routes redirect to. Defaults to this repo's GitHub Releases. The route
105
+ // appends `/download/agent-v<ver>/<asset>` (or `/latest/download/<asset>`).
106
+ agentReleasesBaseUrl: z.string().url().default("https://github.com/Cloudgeni-ai/opengeni/releases"),
107
+ productAccessMode: ProductAccessMode.default("local"),
108
+ billingMode: BillingMode.default("disabled"),
109
+ entitlementsMode: EntitlementsMode.default("none"),
110
+ usageLimitsMode: UsageLimitsMode.default("none"),
111
+ staticEntitlementsJson: z.string().default("{}"),
112
+ staticUsageLimitsJson: z.string().default("{}"),
113
+ delegationSecret: z.string().optional(),
114
+ // Sandbox-surfacing scoped stream-token HMAC secret (master-spine §C.3 / I8).
115
+ // When unset, the API falls back to `delegationSecret` (the same HMAC envelope
116
+ // family, `ogs_` vs `ogd_` prefix). REQUIRED-WHEN-DESKTOP, but the absence of
117
+ // BOTH while sandboxDesktopEnabled=true is a GRACEFUL DEGRADE (DesktopStream
118
+ // transport:null + a loud boot warning), NOT a hard boot-fail (I8/OD-8).
119
+ streamTokenSecret: z.string().optional(),
120
+ // The desktop input plane (raw stream:control writes) is OFF in v1: even a
121
+ // holder of stream:control gets 403 until this flips. Keeps stream:control a
122
+ // declared-but-inert permission so later hardening is a flag flip.
123
+ streamControlEnabled: EnvBoolean.default(false),
124
+ environmentsEncryptionKey: z.string().optional(),
125
+ // Session goal guard rails. Goals are designed for runs that legitimately
126
+ // span days, so length is bounded by pathology detection (no-progress
127
+ // streaks, budget exhaustion), never by count. goalMaxAutoContinuations is
128
+ // therefore UNSET by default (no cap); deployments may configure one, and
129
+ // it then acts as a hard ceiling that per-goal overrides can only lower.
130
+ goalMaxAutoContinuations: z.coerce.number().int().positive().optional(),
131
+ goalNoProgressLimit: z.coerce.number().int().positive().default(3),
132
+ // Per-segment ceiling on agent loop turns (model calls) within a single
133
+ // session turn. Effectively unbounded by default for the same reason as
134
+ // above; the graceful max-turns valve (idle + goal continuation, never a
135
+ // session failure) remains as inert safety should a deployment set a cap.
136
+ agentMaxModelCallsPerTurn: z.coerce.number().int().positive().default(1e6),
137
+ // Where turn-input conversation history comes from (issue #35):
138
+ // "items" (default) = the session_history_items table (SDK-native,
139
+ // version-stable conversation truth); "run_state" = the legacy serialized
140
+ // RunState blob. Items and the sandbox envelope are dual-written
141
+ // unconditionally; this flag governs the read path only, so flipping back to
142
+ // "run_state" remains a safe rollback at any time.
143
+ sessionHistorySource: z.enum(["run_state", "items"]).default("items"),
144
+ // Provider-aware conversation context management (long-lived sessions
145
+ // otherwise grow unbounded until they overflow the model context window and
146
+ // hard-fail every turn). Resolution (see resolveContextCompactionMode):
147
+ // "auto" (default) -> "server" when openaiProvider === "openai" (the
148
+ // OpenAI platform Responses API honors server-side context_management),
149
+ // else "client" (Azure rejects context_management with a 400, so we run
150
+ // our own client-side compaction).
151
+ // "server" / "client" -> force that path regardless of provider.
152
+ // "off" -> neither path (legacy unbounded growth; escape hatch only).
153
+ contextCompactionMode: z.enum(["auto", "server", "client", "off"]).default("auto"),
154
+ // The model's real context window in tokens. gpt-5.5's true window is
155
+ // 1,050,000; it is absent from the SDK's hardcoded compaction window map (it
156
+ // knows only up to gpt-5.4), so the SDK's DynamicCompactionPolicy would fall
157
+ // back to a wrong 240k. We pass an explicit StaticCompactionPolicy threshold
158
+ // derived from these settings on the server path, and use the same numbers to
159
+ // budget the client path.
160
+ contextWindowTokens: z.coerce.number().int().positive().default(105e4),
161
+ // Tokens reserved for model output; subtracted from the window to get the
162
+ // usable input budget B = contextWindowTokens - contextReservedOutputTokens.
163
+ contextReservedOutputTokens: z.coerce.number().int().nonnegative().default(128e3),
164
+ // Server path only: explicit compact_threshold (tokens) handed to the SDK's
165
+ // StaticCompactionPolicy. Defaults to floor(B * contextCompactSoftFraction)
166
+ // when unset.
167
+ contextServerCompactThresholdTokens: z.coerce.number().int().positive().optional(),
168
+ // Client path: compact pre-turn when the last turn's actual input tokens
169
+ // reach softFraction*B; hard-force at hardFraction*B.
170
+ contextCompactSoftFraction: z.coerce.number().positive().max(1).default(0.7),
171
+ contextCompactHardFraction: z.coerce.number().positive().max(1).default(0.85),
172
+ // Client path: tokens of the most recent FULL turns kept verbatim after a
173
+ // compaction (the live working set: recent tool results, not just messages).
174
+ contextKeepRecentTokens: z.coerce.number().int().positive().default(32e3),
175
+ // Client path: token ceiling on the generated summary body.
176
+ contextSummaryMaxTokens: z.coerce.number().int().positive().default(2e4),
177
+ authRequired: EnvBoolean.default(false),
178
+ accessKey: z.string().optional(),
179
+ authAllowHealth: EnvBoolean.default(true),
180
+ authAllowMetrics: EnvBoolean.default(false),
181
+ apiHost: z.string().default("0.0.0.0"),
182
+ apiPort: z.coerce.number().int().positive().default(8e3),
183
+ opengeniMcpUrl: z.string().url().optional(),
184
+ corsAllowOriginRegex: z.string().default(String.raw`^https?://(localhost|127\.0\.0\.1)(:\d+)?$`),
185
+ openaiProvider: z.enum(["openai", "azure"]).default("openai"),
186
+ openaiApiKey: z.string().optional(),
187
+ openaiBaseUrl: z.string().optional(),
188
+ openaiModel: z.string().default("gpt-5.5"),
189
+ openaiAllowedModels: z.string().default("gpt-5.5,gpt-5.4,gpt-5.4-mini"),
190
+ modelPricingJson: z.string().default("{}"),
191
+ // Extra (non-built-in) model providers, declared by the host as a JSON
192
+ // provider registry. Each entry carries its own base URL, API key, wire API
193
+ // ("responses" | "chat") and the models it exposes. The models a client may
194
+ // use are the UNION of the built-in provider's allowed models and every
195
+ // registry provider's models. validateSettings parses this at boot so a
196
+ // malformed registry / unresolvable key / id collision fails fast.
197
+ modelProvidersJson: z.string().default("[]"),
198
+ // Codex (ChatGPT) subscription: when enabled, a per-workspace connected
199
+ // subscription is injected as a synthetic "codex-subscription" registry
200
+ // provider whose models route through the ChatGPT backend (@opengeni/codex).
201
+ codexSubscriptionEnabled: EnvBoolean.default(false),
202
+ // OPENGENI_CODEX_SUBSCRIPTION_ENABLED
203
+ codexProductSku: z.string().optional(),
204
+ // OPENGENI_CODEX_PRODUCT_SKU (X-OpenAI-Product-Sku, apps only)
205
+ // Progressive connector disclosure (Codex-CLI-style tool_search): on a codex
206
+ // turn, flag the ~217 codex_apps connector tools `defer_loading:true` (dropping
207
+ // their schemas from model context) and add one client-executed tool_search
208
+ // tool that BM25-discloses only the matching connectors. Default OFF — a codex
209
+ // turn is byte-for-byte unchanged until enabled. OPENGENI_CODEX_TOOL_SEARCH_ENABLED
210
+ codexToolSearchEnabled: EnvBoolean.default(false),
211
+ // Multi-account P3 (auto-rotation): an account is "near exhaustion" — ineligible to be
212
+ // rotated TO — when EITHER usage window (5h/weekly) is at/over this percent. Default 90 to
213
+ // match the UI danger flip (UsageBar danger at pct >= 90). OPENGENI_CODEX_ROTATION_NEAR_EXHAUSTION_PCT.
214
+ codexRotationNearExhaustionPct: z.coerce.number().int().min(1).max(100).default(90),
215
+ openaiReasoningEffort: ReasoningEffort.default("low"),
216
+ openaiAllowedReasoningEfforts: z.string().default("low,medium,high,xhigh"),
217
+ openaiResponsesTransport: z.enum(["http", "websocket"]).default("http"),
218
+ // Provider-assigned item ids (rs_/msg_/fc_…) in Responses API input are
219
+ // resolved against the provider's server-side response store. That store is
220
+ // not durable enough to anchor long runs on: a response that streamed fine
221
+ // can be missing from the store on the very next model call, which then
222
+ // fails with 400 "Item with id ... not found". "strip" removes the ids from
223
+ // every model-call input so requests are self-contained — conversation
224
+ // truth already lives client-side in session_history_items. "preserve"
225
+ // keeps the SDK's pass-through behavior.
226
+ openaiProviderItemIds: z.enum(["strip", "preserve"]).default("strip"),
227
+ // With ids stripped the provider cannot resolve prior reasoning server-side,
228
+ // so request reasoning.encrypted_content and send it back with each call:
229
+ // reasoning continuity without depending on provider-side storage.
230
+ openaiReasoningEncryptedContent: EnvBoolean.default(true),
231
+ // Model-call retry budget for transient provider failures (429s and friends).
232
+ // The openai client default of 2 retries is too small for sustained TPM
233
+ // backpressure during long autonomous runs.
234
+ openaiMaxRetries: z.coerce.number().int().nonnegative().default(5),
235
+ // Native hosted web search. The live Azure Responses path executes the
236
+ // hosted web_search tool, so this is provider-unconditional: ON by default
237
+ // on every provider, exposed only so operators can disable it. When true,
238
+ // buildOpenGeniAgent attaches webSearchTool() to the agent's tools — it is
239
+ // merged with the MCP-server tools (getAllTools = [...mcpTools, ...tools])
240
+ // and the sandbox capability tools, never replacing them.
241
+ webSearchEnabled: EnvBoolean.default(true),
242
+ // Deployment-default agent persona template (the white-label surface). The
243
+ // runtime resolves the effective template per turn as
244
+ // per-session-override > per-workspace override > this default, substitutes
245
+ // the non-bypassable CORE at AGENT_INSTRUCTIONS_CORE_PLACEHOLDER (or appends
246
+ // it when the template omits the marker), and uses the result as the agent's
247
+ // instructions. Defaulting to DEFAULT_AGENT_INSTRUCTIONS keeps the composed
248
+ // default byte-identical to the historical hardcoded preamble.
249
+ agentInstructionsTemplate: z.string().default(DEFAULT_AGENT_INSTRUCTIONS),
250
+ azureOpenaiBaseUrl: z.string().optional(),
251
+ azureOpenaiEndpoint: z.string().optional(),
252
+ azureOpenaiDeployment: z.string().optional(),
253
+ azureOpenaiApiVersion: z.string().optional(),
254
+ azureOpenaiApiKey: z.string().optional(),
255
+ azureOpenaiAdToken: z.string().optional(),
256
+ disableOpenaiTracing: EnvBoolean.default(false),
257
+ sandboxBackend: SandboxBackend.default("docker"),
258
+ dockerImage: z.string().default("opengeni-sandbox:local"),
259
+ dockerExposedPorts: z.string().default(""),
260
+ dockerNetwork: z.string().optional(),
261
+ modalAppName: z.string().default("opengeni-sandbox"),
262
+ modalImageRef: z.string().optional(),
263
+ // Modal's hard sandbox lifetime (timeoutMs = this * 1000), counted from each
264
+ // create/resume — it is the BACKSTOP that reclaims a box if the reaper/worker is
265
+ // down, NOT the warm-window controller (that's sandboxIdleGraceMs). It must
266
+ // comfortably exceed reaperPeriod + idleGrace so the reaper terminates a
267
+ // genuinely-idle box FIRST; the boot invariant below enforces that. Default 1h
268
+ // (was 900s/15min): the 15-min drain grace counts from the user's LAST release,
269
+ // but Modal's clock starts at the preceding turn's resume — so a 15-min grace on
270
+ // top of a 900s lifetime would let Modal kill the box mid-warm-window. 3600s
271
+ // leaves ~45min of headroom for the active turn before the warm window opens.
272
+ // Knob: OPENGENI_MODAL_TIMEOUT_SECONDS.
273
+ modalTimeoutSeconds: z.coerce.number().int().positive().default(3600),
274
+ modalTokenId: z.string().optional(),
275
+ modalTokenSecret: z.string().optional(),
276
+ modalEnvironment: z.string().optional(),
277
+ // modal gap-fill: idleTimeoutMs + workspacePersistence were unmapped (module 03 §4.1).
278
+ //
279
+ // CRITICAL (sandbox-file-persistence): when this is UNSET the Modal SDK sends
280
+ // idleTimeoutSecs=undefined, so Modal applies its OWN short server-default idle
281
+ // timeout (~minutes) — and a box between turns sits with NO active connection,
282
+ // so that idle clock runs and Modal idle-reaps the box LONG before OpenGeni's
283
+ // own reaper waits out sandboxIdleGraceMs (15min) to resume+persist+terminate
284
+ // it. The observed failure: every drain logs "drainable box already gone
285
+ // (NotFound on resume)", persistWorkspace() never fires, /workspace is lost.
286
+ // Modal's idle-reap is a SECOND reaper racing OpenGeni's — and it wins. The fix:
287
+ // OpenGeni OWNS box lifecycle via its reaper + the hard modalTimeoutSeconds
288
+ // backstop, so the Modal idle-reap must NOT fire first. We default the effective
289
+ // idle timeout to the hard lifetime (effectiveModalIdleTimeoutSeconds), making
290
+ // the box survive its full warm window so the reaper can snapshot it. Set this
291
+ // explicitly (OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS) only to deliberately idle-reap
292
+ // SOONER than the hard lifetime; the boot invariant forbids a value that would
293
+ // reap before reaperPeriod + idleGrace elapses.
294
+ modalIdleTimeoutSeconds: z.coerce.number().int().positive().optional(),
295
+ // /workspace FILE PERSISTENCE across warm/cold cycles. Defaults to
296
+ // `snapshot_filesystem` so EVERY box is created persistence-capable: the reaper
297
+ // snapshots the live box before it terminates a drained group, and a later
298
+ // cold-restore hydrates a fresh box from that snapshot (sandbox-file-persistence).
299
+ // `snapshot_filesystem` requires the manifest declare NO ephemeralPersistencePaths
300
+ // (buildManifest never sets entry.ephemeral, so it never downgrades to tar). Set
301
+ // OPENGENI_MODAL_WORKSPACE_PERSISTENCE=tar to opt back out (no native snapshot;
302
+ // the reaper persists a tar archive — same store+hydrate plumbing, slower).
303
+ modalWorkspacePersistence: z.enum(["tar", "snapshot_filesystem", "snapshot_directory"]).default("snapshot_filesystem"),
304
+ // Snapshot GC backstop (sandbox-file-persistence): the reaper keeps ONE latest
305
+ // filesystem snapshot per lease (delete-prior-on-supersede + delete-on-teardown).
306
+ // This is the TTL retention floor for the periodic orphan sweep — a snapshot
307
+ // whose lease is cold and older than this is best-effort deleted so a crashed
308
+ // persist-then-no-restore never leaks a Modal image. 0 disables the TTL sweep
309
+ // (delete-on-supersede/teardown still run). Default 7 days.
310
+ modalSnapshotRetentionSeconds: z.coerce.number().int().nonnegative().default(604800),
311
+ // Shared desktop toggle: this module reads it for the 6080 port-merge; the
312
+ // owner module (P4.x) acts on it to launch the display stack.
313
+ sandboxDesktopEnabled: EnvBoolean.default(false),
314
+ // Human take-control toggle: when ON (default) the negotiated DesktopStream
315
+ // cell advertises mode "interactive" — the noVNC viewer can drive mouse+keyboard
316
+ // into :0 (x11vnc runs without -viewonly). Turn it OFF for a genuinely read-only
317
+ // deployment: the cell reports mode "read-only" and the client disables the
318
+ // "Take control" affordance. Independent of computerUseReadOnly (the AGENT
319
+ // driver); this gates the HUMAN viewer plane.
320
+ sandboxDesktopInteractive: EnvBoolean.default(true),
321
+ // REAL PTY terminal toggle (P5.t): gates the ttyd pty-ws plane (7681) the API
322
+ // mints over the SAME tunnel as the desktop. Defaults ON — the interactive
323
+ // terminal is a baseline structured-service surface (unlike the heavier desktop
324
+ // pixel plane); a deployment can turn it off to fall back to the read-only
325
+ // sse-events command firehose. The 7681 port-merge tracks sandboxDesktopEnabled
326
+ // (a desktop-capable image is the one that bakes ttyd).
327
+ sandboxTerminalEnabled: EnvBoolean.default(true),
328
+ // The desktop framebuffer geometry the pixel plane advertises + launches the
329
+ // display stack with (P4.2). v1 has no live RANDR resize; a change is a full
330
+ // down→up restart. Defaults match the proven spike geometry (1280x800).
331
+ streamResolutionWidth: z.coerce.number().int().positive().default(1280),
332
+ streamResolutionHeight: z.coerce.number().int().positive().default(800),
333
+ // P4.3 computer-use: the agent drives the SAME :0 humans watch (xdotool/XTEST +
334
+ // scrot). Gated by sandboxDesktopEnabled + a desktop-capable backend in
335
+ // buildAgentCapabilities; computerUseReadOnly:false is the agent-driver default
336
+ // (it must click/type — the human viewer plane is the read-only one).
337
+ computerUseEnabled: EnvBoolean.default(true),
338
+ computerUseReadOnly: EnvBoolean.default(false),
339
+ // P4.3 recording loop: ffmpeg x11grab of :0 → mp4/webm → @opengeni/storage.
340
+ // recordingMaxBytes caps the in-memory finalize buffer (≤ storage single-PUT);
341
+ // recordingMaxSeconds is the ffmpeg -t hard ceiling (bounds a multi-day turn).
342
+ recordingEnabled: EnvBoolean.default(true),
343
+ recordingDefaultCodec: z.enum(["h264-mp4", "vp9-webm"]).default("h264-mp4"),
344
+ recordingFramerate: z.coerce.number().int().positive().default(15),
345
+ recordingMaxSeconds: z.coerce.number().int().positive().default(600),
346
+ recordingMaxBytes: z.coerce.number().int().positive().default(268435456),
347
+ // 256 MB
348
+ // --- daytona ---
349
+ daytonaApiKey: z.string().optional(),
350
+ daytonaApiUrl: z.string().url().optional(),
351
+ daytonaTarget: z.string().optional(),
352
+ daytonaImage: z.string().optional(),
353
+ daytonaSnapshotName: z.string().optional(),
354
+ daytonaAutoStopInterval: z.coerce.number().int().nonnegative().optional(),
355
+ // 0 disables idle-kill
356
+ daytonaTimeoutSeconds: z.coerce.number().int().positive().optional(),
357
+ daytonaExposedPortUrlTtlSeconds: z.coerce.number().int().positive().optional(),
358
+ // --- runloop ---
359
+ runloopApiKey: z.string().optional(),
360
+ runloopBaseUrl: z.string().url().optional(),
361
+ runloopBlueprintName: z.string().optional(),
362
+ runloopBlueprintId: z.string().optional(),
363
+ runloopTunnel: EnvBoolean.default(true),
364
+ runloopKeepAliveSeconds: z.coerce.number().int().positive().optional(),
365
+ // --- e2b (SDK reads E2B_API_KEY from env; mirrored for validation + forwarding) ---
366
+ e2bApiKey: z.string().optional(),
367
+ e2bTemplate: z.string().optional(),
368
+ e2bTimeoutSeconds: z.coerce.number().int().positive().optional(),
369
+ e2bTimeoutAction: z.enum(["pause", "kill"]).optional(),
370
+ e2bAllowInternetAccess: EnvBoolean.optional(),
371
+ e2bAutoResume: EnvBoolean.optional(),
372
+ e2bWorkspacePersistence: z.enum(["tar", "snapshot"]).optional(),
373
+ // --- blaxel ---
374
+ blaxelApiKey: z.string().optional(),
375
+ blaxelImage: z.string().optional(),
376
+ blaxelRegion: z.string().optional(),
377
+ blaxelExposedPortPublic: EnvBoolean.optional(),
378
+ // public vs bl_preview_token
379
+ blaxelExposedPortUrlTtlSeconds: z.coerce.number().int().positive().optional(),
380
+ blaxelMemoryMb: z.coerce.number().int().positive().optional(),
381
+ blaxelTtl: z.string().optional(),
382
+ // --- cloudflare (headless) ---
383
+ cloudflareWorkerUrl: z.string().url().optional(),
384
+ cloudflareApiKey: z.string().optional(),
385
+ // --- vercel (headless) ---
386
+ vercelToken: z.string().optional(),
387
+ vercelProjectId: z.string().optional(),
388
+ vercelTeamId: z.string().optional(),
389
+ vercelRuntime: z.string().optional(),
390
+ // --- sandbox ownership inversion (P1.2 rollout flag, default OFF) ---
391
+ // The keystone flag for the stateless resume-by-id model. When FALSE the
392
+ // agent-turn path is BYTE-FOR-BYTE today's build-and-discard behavior (no
393
+ // lease acquire, no resume-by-id, no non-owned injection). When TRUE the turn
394
+ // activity acquires the group lease, resumes the one box by id from the lease
395
+ // envelope, injects it as a NON-OWNED RunConfig session (the SDK never reaps
396
+ // it — the proven keystone), and releases the holder in finally. Uses
397
+ // EnvBoolean (NOT z.coerce.boolean(), which would coerce "false" -> true and
398
+ // turn the flag ON the moment anyone set the env var to disable it).
399
+ sandboxOwnershipEnabled: EnvBoolean.default(false),
400
+ // --- bring-your-own-compute (selfhosted 11th backend) rollout flag, default OFF ---
401
+ // The keystone flag for the whole selfhosted feature (the enrollment device-flow,
402
+ // the NATS control plane, the relay stream tier). When FALSE the enrollment routes
403
+ // 404 (invisible — the surface does not exist for this deployment) and the
404
+ // selfhosted backend is inert; boot is unaffected. EnvBoolean (NOT
405
+ // z.coerce.boolean(), which coerces "false" -> true). Flipped per-environment via
406
+ // the deploy-staging IaC secret/configmap pattern (dossier §17/§25.1).
407
+ sandboxSelfhostedEnabled: EnvBoolean.default(false),
408
+ // The HMAC secret the control plane signs the enrollment bearer credential with
409
+ // (the `oge_` envelope the agent presents back to the control plane). Optional:
410
+ // when ABSENT and sandboxSelfhostedEnabled is on, the poll route reports the
411
+ // credential plane disabled (graceful degrade, mirrors streamTokenSecret). NEVER
412
+ // logged. Lives in the opengeni-runtime secret (Helm-clobbered configmap avoided).
413
+ enrollmentSigningSecret: z.string().optional(),
414
+ // Connect-info the EnrollmentCredentials hand the agent: the NATS server URL(s)
415
+ // the agent dials for the control plane, and the relay edge base URL for streams.
416
+ // The per-workspace NATS Account creds binding is infra-deferred (M4/relay
417
+ // milestone) — the poll returns these endpoints + a placeholder creds field.
418
+ selfhostedNatsUrl: z.string().optional(),
419
+ selfhostedRelayUrl: z.string().optional(),
420
+ // The HMAC secret the control plane signs the agent's relay PRODUCER token with
421
+ // (the `ogr_` envelope threaded into EnrollmentCredentials.relayToken; M8b/dossier
422
+ // §10.5). The relay verifies the producer token with the SAME secret. Optional:
423
+ // when ABSENT the poll returns an empty relayToken (graceful degrade — the stream
424
+ // plane is simply unavailable until configured). Falls back to streamTokenSecret /
425
+ // delegationSecret (same HMAC family) so a deployment with a stream-token secret
426
+ // needs no second one. NEVER logged. Lives in the opengeni-runtime secret.
427
+ selfhostedRelayTokenSecret: z.string().optional(),
428
+ // The minisign PUBLIC key the agent pins for self-update verification (handed to
429
+ // the agent in EnrollmentCredentials; the SECRET key lives only in CI).
430
+ agentUpdatePublicKey: z.string().optional(),
431
+ // --- NATS auth-callout tenancy boundary (bring-your-own-compute M-AUTH; dossier
432
+ // §10.1 NATS Accounts per workspace + §17 the isolation smoke) -------------
433
+ // nats-server is configured with AUTH CALLOUT: an external agent connects
434
+ // presenting its `oge_` enrollment bearer as the connect auth-token; the server
435
+ // issues an authorization request on $SYS.REQ.USER.AUTH to our responder, which
436
+ // validates the bearer and returns a SIGNED NATS user JWT scoped to pub/sub ONLY
437
+ // `agent.<ws>.>` (+ `_INBOX.>`). That per-subject scope IS the per-workspace
438
+ // isolation. These are deployment-level secrets in the opengeni-runtime secret
439
+ // (Helm-clobbered configmap avoided), all OPTIONAL: when the callout plane is not
440
+ // configured the responder simply does not start (selfhosted agents cannot
441
+ // connect — graceful, never a boot-fail).
442
+ //
443
+ // The callout account SIGNING SEED (`SA...`). Both the user JWT and the
444
+ // authorization-response JWT are signed by this account key; its public key
445
+ // (`A...`) is the `auth_callout.issuer` in the server config. NEVER logged.
446
+ selfhostedNatsCalloutAccountSeed: z.string().optional(),
447
+ // The TARGET ACCOUNT NAME the minted user is placed into (the server-config-mode
448
+ // `auth_callout.account`, e.g. "APP"). The responder writes it as the minted user
449
+ // JWT `aud` so nats-server binds the agent to this account — the SAME account the
450
+ // privileged control plane connects into, so `agent.<ws>.<id>.rpc` request/reply
451
+ // routes. Optional; resolveNatsCalloutConfig defaults it to "APP".
452
+ selfhostedNatsCalloutAccountName: z.string().optional(),
453
+ // The callout RESPONDER's own NATS login (one of the `auth_callout.auth_users`
454
+ // in the AUTH account) — the responder connects with this to subscribe
455
+ // $SYS.REQ.USER.AUTH. Username/password.
456
+ selfhostedNatsCalloutUser: z.string().optional(),
457
+ selfhostedNatsCalloutPassword: z.string().optional(),
458
+ // The PRIVILEGED control-plane login (api/worker): a static account user that may
459
+ // request `agent.*.rpc` + receive its inbox replies. The event bus + the
460
+ // selfhosted control RPC ride THIS connection. Username/password; when unset the
461
+ // bus connects anonymously (local dev / a NATS with no auth_callout).
462
+ selfhostedNatsControlUser: z.string().optional(),
463
+ selfhostedNatsControlPassword: z.string().optional(),
464
+ // --- sandbox lease cadences (cadence invariant validated at boot below) ---
465
+ // reaperPeriod < viewerHolderTTL, and reaperPeriod + idleGrace < the EFFECTIVE
466
+ // box idle timeout (effectiveModalIdleTimeoutSeconds, which defaults to the hard
467
+ // modalTimeoutSeconds). No keep-alive loop: between turns the box survives on its
468
+ // idle timeout — which we pin high enough (via the idle-timeout default) that
469
+ // OpenGeni's reaper, not Modal's idle-reap, governs teardown so /workspace is
470
+ // snapshotted before the box dies (sandbox-file-persistence).
471
+ sandboxLeaseReaperPeriodMs: z.coerce.number().int().positive().default(3e4),
472
+ sandboxViewerHolderTtlMs: z.coerce.number().int().positive().default(9e4),
473
+ // The DRAIN grace: how long a refcount-0 (draining) lease stays WARM before the
474
+ // reaper resume-by-ids the box and terminates it. This is the cost-vs-snappiness
475
+ // dial — when the user navigates away the box keeps refcount 0, but it survives
476
+ // this whole window so a "glanced away then came back" re-arms the SAME warm box
477
+ // (acquireLease re-arms draining->warm; the reaper's BEFORE-terminate re-read
478
+ // skips a re-armed box). Default 15min so a brief detour never cold-creates a
479
+ // fresh EMPTY box; lower it to trade warm cost for a snappier reclaim. Knob:
480
+ // OPENGENI_SANDBOX_IDLE_GRACE_MS.
481
+ sandboxIdleGraceMs: z.coerce.number().int().positive().default(9e5),
482
+ // expires_at refresh window for a held lease (>> the turn 10s heartbeat so a
483
+ // single missed heartbeat never TTL-reaps a live turn). The warming TTL is the
484
+ // window a cold->warming spawner has to commit warm before a reaper resets it.
485
+ sandboxLeaseTtlMs: z.coerce.number().int().positive().default(9e4),
486
+ sandboxLeaseWarmingTtlMs: z.coerce.number().int().positive().default(12e4),
487
+ // --- sandbox warm-time billing (P2.1) ---
488
+ // Per-backend warm rate (usd_micros/sec), like modelPricingJson: an empty {}
489
+ // means warm-cost is not debited (warm-seconds are still metered for audit).
490
+ // Shape: { "modal": 5, "runloop": 4, ... }. Backends absent here meter
491
+ // warm-seconds but accrue NO warm_cost / debit (rate 0).
492
+ sandboxWarmRateMicrosPerSecondJson: z.string().default("{}"),
493
+ // Per-workspace warm cap (cumulative warm-seconds since the start of the UTC
494
+ // month, summed over sandbox.warm_seconds). 0 = unbounded. A workspace over the
495
+ // cap force-drains its VIEWER-ONLY boxes (guarded AND turn_holders=0 — a paying
496
+ // turn is never killed); the reaper then stop()s at refcount 0.
497
+ sandboxMaxWarmSecondsPerWorkspace: z.coerce.number().int().nonnegative().default(0),
498
+ sandboxPreparationProfiles: z.string().default("none"),
499
+ sandboxEnvAllowlist: z.string().default(""),
500
+ objectStorageEndpoint: z.string().url().optional(),
501
+ objectStorageSandboxEndpoint: z.string().url().optional(),
502
+ objectStorageBackend: z.enum(["s3-compatible", "aws-s3", "azure-blob", "gcs"]).default("s3-compatible"),
503
+ objectStorageBucket: z.string().min(1).default("opengeni-files"),
504
+ objectStorageRegion: z.string().min(1).default("us-east-1"),
505
+ objectStorageS3Provider: z.string().min(1).default("Minio"),
506
+ objectStorageAccessKeyId: z.string().optional(),
507
+ objectStorageSecretAccessKey: z.string().optional(),
508
+ objectStorageForcePathStyle: EnvBoolean.default(true),
509
+ objectStorageAzureConnectionString: z.string().optional(),
510
+ objectStorageAzureAccountName: z.string().optional(),
511
+ objectStorageAzureAccountKey: z.string().optional(),
512
+ objectStorageAzureEndpoint: z.string().url().optional(),
513
+ objectStorageGcsProjectId: z.string().optional(),
514
+ objectStorageGcsCredentialsJson: z.string().optional(),
515
+ objectStorageGcsKeyFilename: z.string().optional(),
516
+ objectStorageGcsApiEndpoint: z.string().url().optional(),
517
+ documentParser: z.string().min(1).default("liteparse"),
518
+ documentChunkSize: z.coerce.number().int().positive().default(1200),
519
+ documentChunkOverlap: z.coerce.number().int().nonnegative().default(160),
520
+ documentEmbeddingProvider: z.enum(["openai", "deterministic"]).default("openai"),
521
+ documentEmbeddingModel: z.string().min(1).default("text-embedding-3-large"),
522
+ documentEmbeddingDimensions: z.coerce.number().int().positive().default(3072),
523
+ documentEmbeddingApiKey: z.string().optional(),
524
+ documentEmbeddingBaseUrl: z.string().url().optional(),
525
+ gitAuthorName: z.string().optional(),
526
+ gitAuthorEmail: z.string().optional(),
527
+ gitCommitterName: z.string().optional(),
528
+ gitCommitterEmail: z.string().optional(),
529
+ githubAppManifestBaseUrl: z.string().optional(),
530
+ githubAppManifestStateSecret: z.string().optional(),
531
+ githubAppId: z.string().optional(),
532
+ githubClientId: z.string().optional(),
533
+ githubClientSecret: z.string().optional(),
534
+ githubAppSlug: z.string().optional(),
535
+ githubWebhookSecret: z.string().optional(),
536
+ githubAppPrivateKey: z.string().optional(),
537
+ betterAuthSecret: z.string().optional(),
538
+ betterAuthAllowedHosts: z.string().default(""),
539
+ betterAuthCookieDomain: z.string().optional(),
540
+ betterAuthTrustedOrigins: z.string().default(""),
541
+ resendApiKey: z.string().optional(),
542
+ emailFrom: z.string().default("OpenGeni <auth@mail.opengeni.ai>"),
543
+ stripeSecretKey: z.string().optional(),
544
+ stripePublishableKey: z.string().optional(),
545
+ stripeWebhookSecret: z.string().optional(),
546
+ stripeCreditsProductId: z.string().optional(),
547
+ mcpServers: z.array(z.object({
548
+ id: z.string().min(1).regex(registryId),
549
+ name: z.string().min(1).optional(),
550
+ url: z.string().url(),
551
+ allowedTools: z.array(z.string().min(1)).optional(),
552
+ timeoutMs: z.number().int().positive().optional(),
553
+ cacheToolsList: z.boolean().default(false),
554
+ /**
555
+ * Extra request headers sent to this MCP server (credential injection
556
+ * for workspace-enabled capability MCPs). Populated at runtime from
557
+ * encrypted capability-installation credentials; do not put secrets in
558
+ * OPENGENI_MCP_SERVERS.
559
+ */
560
+ headers: z.record(z.string(), z.string()).optional()
561
+ })).default([])
562
+ });
563
+ var ModelPricingSchema = z.object({
564
+ inputMicrosPerMillionTokens: z.number().int().nonnegative(),
565
+ cachedInputMicrosPerMillionTokens: z.number().int().nonnegative().optional(),
566
+ outputMicrosPerMillionTokens: z.number().int().nonnegative(),
567
+ marginBps: z.number().int().min(0).max(1e5).optional()
568
+ });
569
+ var ModelProviderApi = z.enum(["responses", "chat"]);
570
+ var RegistryProviderKind = z.enum(["api-key", "codex-subscription"]);
571
+ var RegistryModelSchema = z.object({
572
+ id: z.string().min(1),
573
+ // model id sent to the provider, e.g. "accounts/fireworks/models/glm-5p2"
574
+ label: z.string().min(1).optional(),
575
+ // display name; defaults to id
576
+ contextWindowTokens: z.number().int().positive().optional(),
577
+ reasoningEffort: z.boolean().optional(),
578
+ // model accepts a reasoning-effort control
579
+ hostedWebSearch: z.boolean().optional(),
580
+ // provider executes the hosted web_search tool for this model
581
+ pricing: ModelPricingSchema.optional()
582
+ });
583
+ var RegistryProviderSchema = z.object({
584
+ kind: RegistryProviderKind.default("api-key"),
585
+ // "codex-subscription" => per-request token, no static key
586
+ id: z.string().min(1).regex(registryId),
587
+ // stable provider id, e.g. "fireworks"
588
+ label: z.string().min(1).optional(),
589
+ api: ModelProviderApi.default("chat"),
590
+ baseUrl: z.string().url(),
591
+ apiKey: z.string().optional(),
592
+ // inline key (pragmatic) ...
593
+ apiKeyEnv: z.string().optional(),
594
+ // ... OR name of the env var holding the key (preferred)
595
+ defaultQuery: z.record(z.string(), z.string()).optional(),
596
+ defaultHeaders: z.record(z.string(), z.string()).optional(),
597
+ models: z.array(RegistryModelSchema).min(1)
598
+ });
599
+ var defaultModelPricing = {
600
+ "gpt-5.5": {
601
+ inputMicrosPerMillionTokens: 5e6,
602
+ cachedInputMicrosPerMillionTokens: 5e5,
603
+ outputMicrosPerMillionTokens: 3e7,
604
+ marginBps: 2500
605
+ },
606
+ "gpt-5.4": {
607
+ inputMicrosPerMillionTokens: 25e5,
608
+ cachedInputMicrosPerMillionTokens: 25e4,
609
+ outputMicrosPerMillionTokens: 15e6,
610
+ marginBps: 2500
611
+ },
612
+ "gpt-5.4-mini": {
613
+ inputMicrosPerMillionTokens: 75e4,
614
+ cachedInputMicrosPerMillionTokens: 75e3,
615
+ outputMicrosPerMillionTokens: 45e5,
616
+ marginBps: 2500
617
+ },
618
+ "gpt-5.2": {
619
+ inputMicrosPerMillionTokens: 175e4,
620
+ cachedInputMicrosPerMillionTokens: 175e3,
621
+ outputMicrosPerMillionTokens: 14e6,
622
+ marginBps: 2500
623
+ },
624
+ "gpt-5.2-chat-latest": {
625
+ inputMicrosPerMillionTokens: 175e4,
626
+ cachedInputMicrosPerMillionTokens: 175e3,
627
+ outputMicrosPerMillionTokens: 14e6,
628
+ marginBps: 2500
629
+ },
630
+ "gpt-5.2-codex": {
631
+ inputMicrosPerMillionTokens: 175e4,
632
+ cachedInputMicrosPerMillionTokens: 175e3,
633
+ outputMicrosPerMillionTokens: 14e6,
634
+ marginBps: 2500
635
+ },
636
+ "gpt-5.1": {
637
+ inputMicrosPerMillionTokens: 125e4,
638
+ cachedInputMicrosPerMillionTokens: 125e3,
639
+ outputMicrosPerMillionTokens: 1e7,
640
+ marginBps: 2500
641
+ },
642
+ "gpt-5": {
643
+ inputMicrosPerMillionTokens: 125e4,
644
+ cachedInputMicrosPerMillionTokens: 125e3,
645
+ outputMicrosPerMillionTokens: 1e7,
646
+ marginBps: 2500
647
+ },
648
+ "gpt-5-mini": {
649
+ inputMicrosPerMillionTokens: 25e4,
650
+ cachedInputMicrosPerMillionTokens: 25e3,
651
+ outputMicrosPerMillionTokens: 2e6,
652
+ marginBps: 2500
653
+ },
654
+ "gpt-5-nano": {
655
+ inputMicrosPerMillionTokens: 5e4,
656
+ cachedInputMicrosPerMillionTokens: 5e3,
657
+ outputMicrosPerMillionTokens: 4e5,
658
+ marginBps: 2500
659
+ },
660
+ // Fireworks AI / GLM 5.2 — the first shipped non-OpenAI registry model. A
661
+ // built-in default pricing entry makes managed billing work out of the box
662
+ // for hosts that expose this model via OPENGENI_MODEL_PROVIDERS_JSON without
663
+ // also setting OPENGENI_MODEL_PRICING_JSON.
664
+ "accounts/fireworks/models/glm-5p2": {
665
+ inputMicrosPerMillionTokens: 14e5,
666
+ cachedInputMicrosPerMillionTokens: 26e4,
667
+ outputMicrosPerMillionTokens: 44e5,
668
+ marginBps: 2500
669
+ }
670
+ };
671
+ var SANDBOX_REQUIRED_ENV = {
672
+ // docker/local/none need no credentials (local dev container / in-process / off).
673
+ docker: [],
674
+ local: [],
675
+ none: [],
676
+ modal: [
677
+ { field: "modalAppName", env: "OPENGENI_MODAL_APP_NAME" },
678
+ { field: "modalTokenId", env: "OPENGENI_MODAL_TOKEN_ID" },
679
+ { field: "modalTokenSecret", env: "OPENGENI_MODAL_TOKEN_SECRET" }
680
+ ],
681
+ daytona: [
682
+ { field: "daytonaApiKey", env: "OPENGENI_DAYTONA_API_KEY" }
683
+ ],
684
+ runloop: [
685
+ { field: "runloopApiKey", env: "OPENGENI_RUNLOOP_API_KEY" }
686
+ ],
687
+ e2b: [
688
+ { field: "e2bApiKey", env: "OPENGENI_E2B_API_KEY" }
689
+ ],
690
+ blaxel: [
691
+ { field: "blaxelApiKey", env: "OPENGENI_BLAXEL_API_KEY" }
692
+ ],
693
+ cloudflare: [
694
+ { field: "cloudflareWorkerUrl", env: "OPENGENI_CLOUDFLARE_WORKER_URL" }
695
+ ],
696
+ vercel: [
697
+ { field: "vercelToken", env: "OPENGENI_VERCEL_TOKEN" },
698
+ { field: "vercelProjectId", env: "OPENGENI_VERCEL_PROJECT_ID" }
699
+ ],
700
+ // selfhosted needs NO per-box credentials: it is the user's own machine reached
701
+ // over the agent's own enrollment. The enrollment-signing + relay-token secrets
702
+ // are deployment-level (a single runtime secret, not per-active-backend creds),
703
+ // wired in the connectivity/enrollment milestones (M4/M5), not here.
704
+ selfhosted: []
705
+ };
706
+ function requiredSandboxEnvForBackend(backend) {
707
+ return (SANDBOX_REQUIRED_ENV[backend] ?? []).map((entry) => entry.env);
708
+ }
709
+ function optional(name) {
710
+ const value = process.env[name];
711
+ return value && value.trim().length > 0 ? value : void 0;
712
+ }
713
+ function getSettings() {
714
+ const raw = {
715
+ serviceName: optional("OPENGENI_SERVICE_NAME"),
716
+ environment: optional("OPENGENI_ENVIRONMENT"),
717
+ deploymentRevision: optional("OPENGENI_DEPLOYMENT_REVISION") ?? optional("SOURCE_VERSION") ?? optional("GITHUB_SHA"),
718
+ databaseUrl: optional("OPENGENI_DATABASE_URL"),
719
+ dbSchema: optional("OPENGENI_DB_SCHEMA"),
720
+ rlsStrategy: optional("OPENGENI_RLS_STRATEGY"),
721
+ natsUrl: optional("OPENGENI_NATS_URL"),
722
+ temporalHost: optional("OPENGENI_TEMPORAL_HOST"),
723
+ temporalNamespace: optional("OPENGENI_TEMPORAL_NAMESPACE"),
724
+ temporalTaskQueue: optional("OPENGENI_TEMPORAL_TASK_QUEUE"),
725
+ startupDependencyRetryAttempts: optional("OPENGENI_STARTUP_DEPENDENCY_RETRY_ATTEMPTS"),
726
+ startupDependencyRetryInitialDelayMs: optional("OPENGENI_STARTUP_DEPENDENCY_RETRY_INITIAL_DELAY_MS"),
727
+ startupDependencyRetryMaxDelayMs: optional("OPENGENI_STARTUP_DEPENDENCY_RETRY_MAX_DELAY_MS"),
728
+ observabilityStructuredLogs: optional("OPENGENI_OBSERVABILITY_STRUCTURED_LOGS"),
729
+ observabilityMetricsEnabled: optional("OPENGENI_OBSERVABILITY_METRICS_ENABLED"),
730
+ observabilityOtlpEndpoint: optional("OPENGENI_OTEL_EXPORTER_OTLP_ENDPOINT") ?? optional("OTEL_EXPORTER_OTLP_ENDPOINT"),
731
+ observabilityOtlpHeaders: optional("OPENGENI_OTEL_EXPORTER_OTLP_HEADERS") ?? optional("OTEL_EXPORTER_OTLP_HEADERS"),
732
+ publicBaseUrl: optional("OPENGENI_PUBLIC_BASE_URL"),
733
+ agentReleasesBaseUrl: optional("OPENGENI_AGENT_RELEASES_BASE_URL"),
734
+ productAccessMode: optional("OPENGENI_PRODUCT_ACCESS_MODE"),
735
+ billingMode: optional("OPENGENI_BILLING_MODE"),
736
+ entitlementsMode: optional("OPENGENI_ENTITLEMENTS_MODE"),
737
+ usageLimitsMode: optional("OPENGENI_USAGE_LIMITS_MODE"),
738
+ staticEntitlementsJson: optional("OPENGENI_STATIC_ENTITLEMENTS_JSON"),
739
+ staticUsageLimitsJson: optional("OPENGENI_STATIC_USAGE_LIMITS_JSON"),
740
+ delegationSecret: optional("OPENGENI_DELEGATION_SECRET"),
741
+ streamTokenSecret: optional("OPENGENI_STREAM_TOKEN_SECRET"),
742
+ streamControlEnabled: optional("OPENGENI_STREAM_CONTROL_ENABLED"),
743
+ environmentsEncryptionKey: optional("OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY"),
744
+ goalMaxAutoContinuations: optional("OPENGENI_GOAL_MAX_AUTO_CONTINUATIONS"),
745
+ goalNoProgressLimit: optional("OPENGENI_GOAL_NO_PROGRESS_LIMIT"),
746
+ agentMaxModelCallsPerTurn: optional("OPENGENI_AGENT_MAX_MODEL_CALLS_PER_TURN"),
747
+ sessionHistorySource: optional("OPENGENI_SESSION_HISTORY_SOURCE"),
748
+ contextCompactionMode: optional("OPENGENI_CONTEXT_COMPACTION_MODE"),
749
+ contextWindowTokens: optional("OPENGENI_CONTEXT_WINDOW_TOKENS"),
750
+ contextReservedOutputTokens: optional("OPENGENI_CONTEXT_RESERVED_OUTPUT_TOKENS"),
751
+ contextServerCompactThresholdTokens: optional("OPENGENI_CONTEXT_SERVER_COMPACT_THRESHOLD_TOKENS"),
752
+ contextCompactSoftFraction: optional("OPENGENI_CONTEXT_COMPACT_SOFT_FRACTION"),
753
+ contextCompactHardFraction: optional("OPENGENI_CONTEXT_COMPACT_HARD_FRACTION"),
754
+ contextKeepRecentTokens: optional("OPENGENI_CONTEXT_KEEP_RECENT_TOKENS"),
755
+ contextSummaryMaxTokens: optional("OPENGENI_CONTEXT_SUMMARY_MAX_TOKENS"),
756
+ authRequired: optional("OPENGENI_AUTH_REQUIRED"),
757
+ accessKey: optional("OPENGENI_ACCESS_KEY"),
758
+ authAllowHealth: optional("OPENGENI_AUTH_ALLOW_HEALTH"),
759
+ authAllowMetrics: optional("OPENGENI_AUTH_ALLOW_METRICS"),
760
+ apiHost: optional("OPENGENI_API_HOST"),
761
+ apiPort: optional("OPENGENI_API_PORT"),
762
+ opengeniMcpUrl: optional("OPENGENI_MCP_URL"),
763
+ corsAllowOriginRegex: optional("OPENGENI_CORS_ALLOW_ORIGIN_REGEX"),
764
+ openaiProvider: optional("OPENGENI_OPENAI_PROVIDER"),
765
+ openaiApiKey: optional("OPENGENI_OPENAI_API_KEY") ?? optional("OPENAI_API_KEY"),
766
+ openaiBaseUrl: optional("OPENGENI_OPENAI_BASE_URL") ?? optional("OPENAI_BASE_URL"),
767
+ openaiModel: optional("OPENGENI_OPENAI_MODEL"),
768
+ openaiAllowedModels: optional("OPENGENI_OPENAI_ALLOWED_MODELS"),
769
+ modelPricingJson: optional("OPENGENI_MODEL_PRICING_JSON"),
770
+ modelProvidersJson: optional("OPENGENI_MODEL_PROVIDERS_JSON"),
771
+ codexSubscriptionEnabled: optional("OPENGENI_CODEX_SUBSCRIPTION_ENABLED"),
772
+ codexToolSearchEnabled: optional("OPENGENI_CODEX_TOOL_SEARCH_ENABLED"),
773
+ codexProductSku: optional("OPENGENI_CODEX_PRODUCT_SKU"),
774
+ codexRotationNearExhaustionPct: optional("OPENGENI_CODEX_ROTATION_NEAR_EXHAUSTION_PCT"),
775
+ openaiReasoningEffort: optional("OPENGENI_OPENAI_REASONING_EFFORT"),
776
+ openaiAllowedReasoningEfforts: optional("OPENGENI_OPENAI_ALLOWED_REASONING_EFFORTS"),
777
+ openaiResponsesTransport: optional("OPENGENI_OPENAI_RESPONSES_TRANSPORT"),
778
+ openaiProviderItemIds: optional("OPENGENI_OPENAI_PROVIDER_ITEM_IDS"),
779
+ openaiReasoningEncryptedContent: optional("OPENGENI_OPENAI_REASONING_ENCRYPTED_CONTENT"),
780
+ openaiMaxRetries: optional("OPENGENI_OPENAI_MAX_RETRIES"),
781
+ webSearchEnabled: optional("OPENGENI_WEB_SEARCH_ENABLED"),
782
+ agentInstructionsTemplate: optional("OPENGENI_AGENT_INSTRUCTIONS_TEMPLATE"),
783
+ azureOpenaiBaseUrl: optional("OPENGENI_AZURE_OPENAI_BASE_URL"),
784
+ azureOpenaiEndpoint: optional("OPENGENI_AZURE_OPENAI_ENDPOINT"),
785
+ azureOpenaiDeployment: optional("OPENGENI_AZURE_OPENAI_DEPLOYMENT"),
786
+ azureOpenaiApiVersion: optional("OPENGENI_AZURE_OPENAI_API_VERSION"),
787
+ azureOpenaiApiKey: optional("OPENGENI_AZURE_OPENAI_API_KEY"),
788
+ azureOpenaiAdToken: optional("OPENGENI_AZURE_OPENAI_AD_TOKEN"),
789
+ disableOpenaiTracing: optional("OPENGENI_DISABLE_OPENAI_TRACING"),
790
+ sandboxBackend: optional("OPENGENI_SANDBOX_BACKEND"),
791
+ dockerImage: optional("OPENGENI_DOCKER_IMAGE"),
792
+ dockerExposedPorts: optional("OPENGENI_DOCKER_EXPOSED_PORTS"),
793
+ dockerNetwork: optional("OPENGENI_DOCKER_NETWORK"),
794
+ modalAppName: optional("OPENGENI_MODAL_APP_NAME"),
795
+ modalImageRef: optional("OPENGENI_MODAL_IMAGE_REF"),
796
+ modalTimeoutSeconds: optional("OPENGENI_MODAL_TIMEOUT_SECONDS"),
797
+ modalTokenId: optional("OPENGENI_MODAL_TOKEN_ID"),
798
+ modalTokenSecret: optional("OPENGENI_MODAL_TOKEN_SECRET"),
799
+ modalEnvironment: optional("OPENGENI_MODAL_ENVIRONMENT"),
800
+ modalIdleTimeoutSeconds: optional("OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS"),
801
+ modalWorkspacePersistence: optional("OPENGENI_MODAL_WORKSPACE_PERSISTENCE"),
802
+ modalSnapshotRetentionSeconds: optional("OPENGENI_MODAL_SNAPSHOT_RETENTION_SECONDS"),
803
+ sandboxDesktopEnabled: optional("OPENGENI_SANDBOX_DESKTOP_ENABLED"),
804
+ sandboxDesktopInteractive: optional("OPENGENI_SANDBOX_DESKTOP_INTERACTIVE"),
805
+ sandboxTerminalEnabled: optional("OPENGENI_SANDBOX_TERMINAL_ENABLED"),
806
+ streamResolutionWidth: optional("OPENGENI_STREAM_RESOLUTION_WIDTH"),
807
+ streamResolutionHeight: optional("OPENGENI_STREAM_RESOLUTION_HEIGHT"),
808
+ computerUseEnabled: optional("OPENGENI_COMPUTER_USE_ENABLED"),
809
+ computerUseReadOnly: optional("OPENGENI_COMPUTER_USE_READONLY"),
810
+ recordingEnabled: optional("OPENGENI_RECORDING_ENABLED"),
811
+ recordingDefaultCodec: optional("OPENGENI_RECORDING_DEFAULT_CODEC"),
812
+ recordingFramerate: optional("OPENGENI_RECORDING_FRAMERATE"),
813
+ recordingMaxSeconds: optional("OPENGENI_RECORDING_MAX_SECONDS"),
814
+ recordingMaxBytes: optional("OPENGENI_RECORDING_MAX_BYTES"),
815
+ daytonaApiKey: optional("OPENGENI_DAYTONA_API_KEY"),
816
+ daytonaApiUrl: optional("OPENGENI_DAYTONA_API_URL"),
817
+ daytonaTarget: optional("OPENGENI_DAYTONA_TARGET"),
818
+ daytonaImage: optional("OPENGENI_DAYTONA_IMAGE"),
819
+ daytonaSnapshotName: optional("OPENGENI_DAYTONA_SNAPSHOT_NAME"),
820
+ daytonaAutoStopInterval: optional("OPENGENI_DAYTONA_AUTO_STOP_INTERVAL"),
821
+ daytonaTimeoutSeconds: optional("OPENGENI_DAYTONA_TIMEOUT_SECONDS"),
822
+ daytonaExposedPortUrlTtlSeconds: optional("OPENGENI_DAYTONA_EXPOSED_PORT_URL_TTL_SECONDS"),
823
+ runloopApiKey: optional("OPENGENI_RUNLOOP_API_KEY"),
824
+ runloopBaseUrl: optional("OPENGENI_RUNLOOP_BASE_URL"),
825
+ runloopBlueprintName: optional("OPENGENI_RUNLOOP_BLUEPRINT_NAME"),
826
+ runloopBlueprintId: optional("OPENGENI_RUNLOOP_BLUEPRINT_ID"),
827
+ runloopTunnel: optional("OPENGENI_RUNLOOP_TUNNEL"),
828
+ runloopKeepAliveSeconds: optional("OPENGENI_RUNLOOP_KEEP_ALIVE_SECONDS"),
829
+ e2bApiKey: optional("OPENGENI_E2B_API_KEY"),
830
+ e2bTemplate: optional("OPENGENI_E2B_TEMPLATE"),
831
+ e2bTimeoutSeconds: optional("OPENGENI_E2B_TIMEOUT_SECONDS"),
832
+ e2bTimeoutAction: optional("OPENGENI_E2B_TIMEOUT_ACTION"),
833
+ e2bAllowInternetAccess: optional("OPENGENI_E2B_ALLOW_INTERNET_ACCESS"),
834
+ e2bAutoResume: optional("OPENGENI_E2B_AUTO_RESUME"),
835
+ e2bWorkspacePersistence: optional("OPENGENI_E2B_WORKSPACE_PERSISTENCE"),
836
+ blaxelApiKey: optional("OPENGENI_BLAXEL_API_KEY"),
837
+ blaxelImage: optional("OPENGENI_BLAXEL_IMAGE"),
838
+ blaxelRegion: optional("OPENGENI_BLAXEL_REGION"),
839
+ blaxelExposedPortPublic: optional("OPENGENI_BLAXEL_EXPOSED_PORT_PUBLIC"),
840
+ blaxelExposedPortUrlTtlSeconds: optional("OPENGENI_BLAXEL_EXPOSED_PORT_URL_TTL_SECONDS"),
841
+ blaxelMemoryMb: optional("OPENGENI_BLAXEL_MEMORY_MB"),
842
+ blaxelTtl: optional("OPENGENI_BLAXEL_TTL"),
843
+ cloudflareWorkerUrl: optional("OPENGENI_CLOUDFLARE_WORKER_URL"),
844
+ cloudflareApiKey: optional("OPENGENI_CLOUDFLARE_API_KEY"),
845
+ vercelToken: optional("OPENGENI_VERCEL_TOKEN"),
846
+ vercelProjectId: optional("OPENGENI_VERCEL_PROJECT_ID"),
847
+ vercelTeamId: optional("OPENGENI_VERCEL_TEAM_ID"),
848
+ vercelRuntime: optional("OPENGENI_VERCEL_RUNTIME"),
849
+ sandboxOwnershipEnabled: optional("OPENGENI_SANDBOX_OWNERSHIP_ENABLED"),
850
+ sandboxSelfhostedEnabled: optional("OPENGENI_SANDBOX_SELFHOSTED_ENABLED"),
851
+ enrollmentSigningSecret: optional("OPENGENI_ENROLLMENT_SIGNING_SECRET"),
852
+ selfhostedNatsUrl: optional("OPENGENI_SELFHOSTED_NATS_URL"),
853
+ selfhostedRelayUrl: optional("OPENGENI_SELFHOSTED_RELAY_URL"),
854
+ selfhostedRelayTokenSecret: optional("OPENGENI_SELFHOSTED_RELAY_TOKEN_SECRET"),
855
+ agentUpdatePublicKey: optional("OPENGENI_AGENT_UPDATE_PUBLIC_KEY"),
856
+ selfhostedNatsCalloutAccountSeed: optional("OPENGENI_SELFHOSTED_NATS_CALLOUT_ACCOUNT_SEED"),
857
+ selfhostedNatsCalloutAccountName: optional("OPENGENI_SELFHOSTED_NATS_CALLOUT_ACCOUNT_NAME"),
858
+ selfhostedNatsCalloutUser: optional("OPENGENI_SELFHOSTED_NATS_CALLOUT_USER"),
859
+ selfhostedNatsCalloutPassword: optional("OPENGENI_SELFHOSTED_NATS_CALLOUT_PASSWORD"),
860
+ selfhostedNatsControlUser: optional("OPENGENI_SELFHOSTED_NATS_CONTROL_USER"),
861
+ selfhostedNatsControlPassword: optional("OPENGENI_SELFHOSTED_NATS_CONTROL_PASSWORD"),
862
+ sandboxLeaseReaperPeriodMs: optional("OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS"),
863
+ sandboxViewerHolderTtlMs: optional("OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS"),
864
+ sandboxIdleGraceMs: optional("OPENGENI_SANDBOX_IDLE_GRACE_MS"),
865
+ sandboxLeaseTtlMs: optional("OPENGENI_SANDBOX_LEASE_TTL_MS"),
866
+ sandboxLeaseWarmingTtlMs: optional("OPENGENI_SANDBOX_LEASE_WARMING_TTL_MS"),
867
+ sandboxWarmRateMicrosPerSecondJson: optional("OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON"),
868
+ sandboxMaxWarmSecondsPerWorkspace: optional("OPENGENI_SANDBOX_MAX_WARM_SECONDS_PER_WORKSPACE"),
869
+ sandboxPreparationProfiles: optional("OPENGENI_SANDBOX_PREPARATION_PROFILES"),
870
+ sandboxEnvAllowlist: optional("OPENGENI_SANDBOX_ENV_ALLOWLIST"),
871
+ objectStorageEndpoint: optional("OPENGENI_OBJECT_STORAGE_ENDPOINT"),
872
+ objectStorageSandboxEndpoint: optional("OPENGENI_OBJECT_STORAGE_SANDBOX_ENDPOINT"),
873
+ objectStorageBackend: optional("OPENGENI_OBJECT_STORAGE_BACKEND"),
874
+ objectStorageBucket: optional("OPENGENI_OBJECT_STORAGE_BUCKET"),
875
+ objectStorageRegion: optional("OPENGENI_OBJECT_STORAGE_REGION"),
876
+ objectStorageS3Provider: optional("OPENGENI_OBJECT_STORAGE_S3_PROVIDER"),
877
+ objectStorageAccessKeyId: optional("OPENGENI_OBJECT_STORAGE_ACCESS_KEY_ID"),
878
+ objectStorageSecretAccessKey: optional("OPENGENI_OBJECT_STORAGE_SECRET_ACCESS_KEY"),
879
+ objectStorageForcePathStyle: optional("OPENGENI_OBJECT_STORAGE_FORCE_PATH_STYLE"),
880
+ objectStorageAzureConnectionString: optional("OPENGENI_OBJECT_STORAGE_AZURE_CONNECTION_STRING"),
881
+ objectStorageAzureAccountName: optional("OPENGENI_OBJECT_STORAGE_AZURE_ACCOUNT_NAME"),
882
+ objectStorageAzureAccountKey: optional("OPENGENI_OBJECT_STORAGE_AZURE_ACCOUNT_KEY"),
883
+ objectStorageAzureEndpoint: optional("OPENGENI_OBJECT_STORAGE_AZURE_ENDPOINT"),
884
+ objectStorageGcsProjectId: optional("OPENGENI_OBJECT_STORAGE_GCS_PROJECT_ID"),
885
+ objectStorageGcsCredentialsJson: optional("OPENGENI_OBJECT_STORAGE_GCS_CREDENTIALS_JSON"),
886
+ objectStorageGcsKeyFilename: optional("OPENGENI_OBJECT_STORAGE_GCS_KEY_FILENAME"),
887
+ objectStorageGcsApiEndpoint: optional("OPENGENI_OBJECT_STORAGE_GCS_API_ENDPOINT"),
888
+ documentParser: optional("OPENGENI_DOCUMENT_PARSER"),
889
+ documentChunkSize: optional("OPENGENI_DOCUMENT_CHUNK_SIZE"),
890
+ documentChunkOverlap: optional("OPENGENI_DOCUMENT_CHUNK_OVERLAP"),
891
+ documentEmbeddingProvider: optional("OPENGENI_DOCUMENT_EMBEDDING_PROVIDER"),
892
+ documentEmbeddingModel: optional("OPENGENI_DOCUMENT_EMBEDDING_MODEL"),
893
+ documentEmbeddingDimensions: optional("OPENGENI_DOCUMENT_EMBEDDING_DIMENSIONS"),
894
+ documentEmbeddingApiKey: optional("OPENGENI_DOCUMENT_EMBEDDING_API_KEY"),
895
+ documentEmbeddingBaseUrl: optional("OPENGENI_DOCUMENT_EMBEDDING_BASE_URL"),
896
+ gitAuthorName: optional("OPENGENI_GIT_AUTHOR_NAME"),
897
+ gitAuthorEmail: optional("OPENGENI_GIT_AUTHOR_EMAIL"),
898
+ gitCommitterName: optional("OPENGENI_GIT_COMMITTER_NAME"),
899
+ gitCommitterEmail: optional("OPENGENI_GIT_COMMITTER_EMAIL"),
900
+ githubAppManifestBaseUrl: optional("OPENGENI_GITHUB_APP_MANIFEST_BASE_URL"),
901
+ githubAppManifestStateSecret: optional("OPENGENI_GITHUB_APP_MANIFEST_STATE_SECRET"),
902
+ githubAppId: optional("OPENGENI_GITHUB_APP_ID"),
903
+ githubClientId: optional("OPENGENI_GITHUB_CLIENT_ID"),
904
+ githubClientSecret: optional("OPENGENI_GITHUB_CLIENT_SECRET"),
905
+ githubAppSlug: optional("OPENGENI_GITHUB_APP_SLUG"),
906
+ githubWebhookSecret: optional("OPENGENI_GITHUB_WEBHOOK_SECRET"),
907
+ githubAppPrivateKey: optional("OPENGENI_GITHUB_APP_PRIVATE_KEY"),
908
+ betterAuthSecret: optional("OPENGENI_BETTER_AUTH_SECRET"),
909
+ betterAuthAllowedHosts: optional("OPENGENI_BETTER_AUTH_ALLOWED_HOSTS"),
910
+ betterAuthCookieDomain: optional("OPENGENI_BETTER_AUTH_COOKIE_DOMAIN"),
911
+ betterAuthTrustedOrigins: optional("OPENGENI_BETTER_AUTH_TRUSTED_ORIGINS"),
912
+ resendApiKey: optional("OPENGENI_RESEND_API_KEY"),
913
+ emailFrom: optional("OPENGENI_EMAIL_FROM"),
914
+ stripeSecretKey: optional("OPENGENI_STRIPE_SECRET_KEY"),
915
+ stripePublishableKey: optional("OPENGENI_STRIPE_PUBLISHABLE_KEY"),
916
+ stripeWebhookSecret: optional("OPENGENI_STRIPE_WEBHOOK_SECRET"),
917
+ stripeCreditsProductId: optional("OPENGENI_STRIPE_CREDITS_PRODUCT_ID"),
918
+ mcpServers: parseMcpServers(optional("OPENGENI_MCP_SERVERS"))
919
+ };
920
+ const parsed = SettingsSchema.parse(raw);
921
+ const settings = {
922
+ ...parsed,
923
+ mcpServers: ensureBuiltInMcpServers(parsed)
924
+ };
925
+ validateSettings(settings);
926
+ return settings;
927
+ }
928
+ function effectiveModalIdleTimeoutSeconds(settings) {
929
+ return settings.modalIdleTimeoutSeconds ?? settings.modalTimeoutSeconds;
930
+ }
931
+ function collectSandboxEnvironment(settings, source = process.env) {
932
+ const out = {};
933
+ for (const name of sandboxEnvironmentVariableNames(settings)) {
934
+ const value = source[name];
935
+ if (value) {
936
+ out[name] = value;
937
+ }
938
+ }
939
+ return out;
940
+ }
941
+ function resolveProviderApiKey(provider, source = process.env) {
942
+ if (provider.apiKey) {
943
+ return provider.apiKey;
944
+ }
945
+ if (provider.apiKeyEnv) {
946
+ const value = source[provider.apiKeyEnv];
947
+ return value && value.trim().length > 0 ? value : void 0;
948
+ }
949
+ return void 0;
950
+ }
951
+ function builtinProviderId(settings) {
952
+ return settings.openaiProvider === "azure" ? "azure" : "openai";
953
+ }
954
+ function builtinProviderLabel(settings) {
955
+ return settings.openaiProvider === "azure" ? "Azure OpenAI" : "OpenAI";
956
+ }
957
+ function configuredProviders(settings) {
958
+ const builtin = {
959
+ id: builtinProviderId(settings),
960
+ label: builtinProviderLabel(settings),
961
+ kind: "api-key",
962
+ api: "responses",
963
+ builtin: true,
964
+ compactionMode: resolveContextCompactionMode(settings)
965
+ };
966
+ if (settings.openaiProvider === "azure") {
967
+ builtin.baseUrl = settings.azureOpenaiBaseUrl ?? settings.azureOpenaiEndpoint;
968
+ builtin.apiKey = settings.azureOpenaiApiKey ?? settings.azureOpenaiAdToken;
969
+ } else {
970
+ builtin.baseUrl = settings.openaiBaseUrl;
971
+ builtin.apiKey = settings.openaiApiKey;
972
+ }
973
+ const registry = parseModelProvidersJson(settings.modelProvidersJson).map((provider) => ({
974
+ id: provider.id,
975
+ label: provider.label ?? provider.id,
976
+ kind: provider.kind,
977
+ api: provider.api,
978
+ builtin: false,
979
+ baseUrl: provider.baseUrl,
980
+ apiKey: resolveProviderApiKey(provider),
981
+ defaultQuery: provider.defaultQuery,
982
+ defaultHeaders: provider.defaultHeaders,
983
+ compactionMode: "client"
984
+ }));
985
+ return [builtin, ...registry];
986
+ }
987
+ function configuredModels(settings) {
988
+ const builtinId = builtinProviderId(settings);
989
+ const builtinLabel = builtinProviderLabel(settings);
990
+ const registryOwnedIds = new Set(
991
+ parseModelProvidersJson(settings.modelProvidersJson).flatMap((provider) => provider.models.map((model) => model.id))
992
+ );
993
+ const isRegistryNamespaced = (id) => id.startsWith(CODEX_MODEL_ID_PREFIX) || id.includes("/") && registryOwnedIds.has(id);
994
+ const out = uniqueValues([settings.openaiModel, ...splitCsv(settings.openaiAllowedModels)]).filter((id) => !isRegistryNamespaced(id)).map((id) => ({
995
+ id,
996
+ label: id,
997
+ providerId: builtinId,
998
+ providerLabel: builtinLabel,
999
+ api: "responses",
1000
+ contextWindowTokens: settings.contextWindowTokens,
1001
+ reasoningEffort: true,
1002
+ hostedWebSearch: settings.webSearchEnabled
1003
+ }));
1004
+ for (const provider of parseModelProvidersJson(settings.modelProvidersJson)) {
1005
+ const providerLabel = provider.label ?? provider.id;
1006
+ for (const model of provider.models) {
1007
+ out.push({
1008
+ id: model.id,
1009
+ label: model.label ?? model.id,
1010
+ providerId: provider.id,
1011
+ providerLabel,
1012
+ api: provider.api,
1013
+ ...model.contextWindowTokens === void 0 ? {} : { contextWindowTokens: model.contextWindowTokens },
1014
+ reasoningEffort: model.reasoningEffort ?? false,
1015
+ hostedWebSearch: model.hostedWebSearch ?? false
1016
+ });
1017
+ }
1018
+ }
1019
+ const seen = /* @__PURE__ */ new Set();
1020
+ return out.filter((model) => {
1021
+ if (seen.has(model.id)) {
1022
+ return false;
1023
+ }
1024
+ seen.add(model.id);
1025
+ return true;
1026
+ });
1027
+ }
1028
+ function configuredAllowedModels(settings) {
1029
+ return configuredModels(settings).map((model) => model.id);
1030
+ }
1031
+ function resolveModelProvider(settings, modelId) {
1032
+ const model = configuredModels(settings).find((candidate) => candidate.id === modelId);
1033
+ if (!model) {
1034
+ return void 0;
1035
+ }
1036
+ const provider = configuredProviders(settings).find((candidate) => candidate.id === model.providerId);
1037
+ if (!provider) {
1038
+ return void 0;
1039
+ }
1040
+ return { provider, model };
1041
+ }
1042
+ function configuredModelPricing(settings) {
1043
+ const registry = {};
1044
+ for (const provider of parseModelProvidersJson(settings.modelProvidersJson)) {
1045
+ for (const model of provider.models) {
1046
+ if (model.pricing) {
1047
+ registry[model.id] = model.pricing;
1048
+ }
1049
+ }
1050
+ }
1051
+ const configured = parseModelPricingJson(settings.modelPricingJson);
1052
+ return {
1053
+ ...defaultModelPricing,
1054
+ ...registry,
1055
+ ...configured
1056
+ };
1057
+ }
1058
+ function resolveContextCompactionMode(settings) {
1059
+ switch (settings.contextCompactionMode) {
1060
+ case "server":
1061
+ return "server";
1062
+ case "client":
1063
+ return "client";
1064
+ case "off":
1065
+ return "off";
1066
+ case "auto":
1067
+ default:
1068
+ return settings.openaiProvider === "openai" ? "server" : "client";
1069
+ }
1070
+ }
1071
+ function contextInputBudgetTokens(settings) {
1072
+ return Math.max(0, settings.contextWindowTokens - settings.contextReservedOutputTokens);
1073
+ }
1074
+ function contextServerCompactThreshold(settings) {
1075
+ if (settings.contextServerCompactThresholdTokens) {
1076
+ return settings.contextServerCompactThresholdTokens;
1077
+ }
1078
+ return Math.floor(contextInputBudgetTokens(settings) * settings.contextCompactSoftFraction);
1079
+ }
1080
+ function configuredStaticUsageLimits(settings) {
1081
+ return parseStaticUsageLimitsJson(settings.staticUsageLimitsJson);
1082
+ }
1083
+ function configuredEntitlements(settings) {
1084
+ if (settings.entitlementsMode === "none") {
1085
+ return {};
1086
+ }
1087
+ const configured = parseStaticEntitlementsJson(settings.staticEntitlementsJson);
1088
+ if (settings.entitlementsMode === "static") {
1089
+ return configured;
1090
+ }
1091
+ return {
1092
+ "managed.auth.email_password": true,
1093
+ "managed.billing.prepaid_credits": settings.billingMode === "stripe",
1094
+ "managed.api_keys": true,
1095
+ "managed.workspaces": true,
1096
+ "managed.github_app": Boolean(settings.githubAppId && settings.githubAppPrivateKey),
1097
+ ...configured
1098
+ };
1099
+ }
1100
+ function calculateModelUsageCostMicros(settings, model, usage) {
1101
+ const pricing = configuredModelPricing(settings)[model];
1102
+ if (!pricing) {
1103
+ throw new Error(`Missing model pricing for ${model}`);
1104
+ }
1105
+ const entries = usage.requestUsageEntries && usage.requestUsageEntries.length > 0 ? usage.requestUsageEntries : [usage];
1106
+ const rawCost = entries.reduce((sum, entry) => sum + calculateEntryCostMicros(pricing, entry), 0);
1107
+ const marginBps = pricing.marginBps ?? 0;
1108
+ return Math.ceil(rawCost * (1e4 + marginBps) / 1e4);
1109
+ }
1110
+ function configuredAllowedReasoningEfforts(settings) {
1111
+ return uniqueValues([settings.openaiReasoningEffort, ...splitCsv(settings.openaiAllowedReasoningEfforts)]).map((value) => ReasoningEffort.parse(value));
1112
+ }
1113
+ function environmentsEncryptionKeyBytes(settings) {
1114
+ if (!settings.environmentsEncryptionKey) {
1115
+ return null;
1116
+ }
1117
+ const decoded = Buffer.from(settings.environmentsEncryptionKey, "base64");
1118
+ if (decoded.length !== 32) {
1119
+ throw new Error("OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY must be base64 for exactly 32 bytes (generate with: openssl rand -base64 32)");
1120
+ }
1121
+ return new Uint8Array(decoded);
1122
+ }
1123
+ function dbSearchPath(settings) {
1124
+ const schema = settings.dbSchema?.trim();
1125
+ if (!schema) {
1126
+ return void 0;
1127
+ }
1128
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(schema)) {
1129
+ throw new Error(`OPENGENI_DB_SCHEMA is not a valid Postgres identifier: ${schema}`);
1130
+ }
1131
+ return `${schema},opengeni_private,public`;
1132
+ }
1133
+ function collectGitIdentityEnvironment(settings) {
1134
+ return Object.fromEntries(Object.entries({
1135
+ GIT_AUTHOR_NAME: settings.gitAuthorName,
1136
+ GIT_AUTHOR_EMAIL: settings.gitAuthorEmail,
1137
+ GIT_COMMITTER_NAME: settings.gitCommitterName ?? settings.gitAuthorName,
1138
+ GIT_COMMITTER_EMAIL: settings.gitCommitterEmail ?? settings.gitAuthorEmail
1139
+ }).filter((entry) => typeof entry[1] === "string" && entry[1].trim().length > 0));
1140
+ }
1141
+ function stableSandboxEnvironmentForRun(settings, workspaceEnvironment = {}) {
1142
+ const environment = {
1143
+ ...collectSandboxEnvironment(settings),
1144
+ ...collectGitIdentityEnvironment(settings),
1145
+ ...workspaceEnvironment
1146
+ };
1147
+ const descriptor = CAPABILITY_DESCRIPTORS[settings.sandboxBackend];
1148
+ if (settings.sandboxBackend !== "none" && settings.sandboxBackend !== "local") {
1149
+ environment.HOME ??= descriptor.workspaceRoot;
1150
+ }
1151
+ environment.OPENGENI_GIT_TOKEN_FILE ??= `${environment.HOME ?? descriptor.workspaceRoot}/.opengeni/git-token`;
1152
+ return environment;
1153
+ }
1154
+ function hasGitHubRepositorySelection(resources) {
1155
+ const positive = (value) => typeof value === "number" && Number.isInteger(value) && value > 0 || typeof value === "string" && /^\d+$/.test(value) && Number(value) > 0;
1156
+ return resources.some((resource) => resource.kind === "repository" && positive(resource.githubInstallationId) && positive(resource.githubRepositoryId));
1157
+ }
1158
+ function applyGitAuthPointerEnvironment(environment, identity) {
1159
+ environment.GIT_ASKPASS = `${environment.HOME ?? "/workspace"}/.opengeni/askpass`;
1160
+ environment.GIT_TERMINAL_PROMPT = "0";
1161
+ if (identity) {
1162
+ environment.GIT_AUTHOR_NAME = environment.GIT_AUTHOR_NAME || identity.name;
1163
+ environment.GIT_AUTHOR_EMAIL = environment.GIT_AUTHOR_EMAIL || identity.email;
1164
+ environment.GIT_COMMITTER_NAME = environment.GIT_COMMITTER_NAME || identity.name;
1165
+ environment.GIT_COMMITTER_EMAIL = environment.GIT_COMMITTER_EMAIL || identity.email;
1166
+ }
1167
+ return environment;
1168
+ }
1169
+ function startupRetryOptions(settings) {
1170
+ return {
1171
+ attempts: settings.startupDependencyRetryAttempts,
1172
+ initialDelayMs: settings.startupDependencyRetryInitialDelayMs,
1173
+ maxDelayMs: settings.startupDependencyRetryMaxDelayMs
1174
+ };
1175
+ }
1176
+ async function retryStartupDependency(label, operation, options = {}) {
1177
+ const attempts = Math.max(1, Math.floor(options.attempts ?? 30));
1178
+ const initialDelayMs = Math.max(0, Math.floor(options.initialDelayMs ?? 1e3));
1179
+ const maxDelayMs = Math.max(initialDelayMs, Math.floor(options.maxDelayMs ?? 5e3));
1180
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
1181
+ try {
1182
+ return await operation();
1183
+ } catch (error) {
1184
+ if (attempt >= attempts) {
1185
+ throw error;
1186
+ }
1187
+ const delayMs = Math.min(maxDelayMs, initialDelayMs * 2 ** (attempt - 1));
1188
+ options.onRetry?.({ label, attempt, attempts, delayMs, error });
1189
+ await delay(delayMs);
1190
+ }
1191
+ }
1192
+ throw new Error(`unreachable startup retry state for ${label}`);
1193
+ }
1194
+ function sandboxEnvironmentVariableNames(settings) {
1195
+ const profiles = sandboxPreparationProfileNames(settings);
1196
+ const names = [];
1197
+ for (const profile of profiles) {
1198
+ names.push(...sandboxPreparationProfiles[profile].env);
1199
+ }
1200
+ names.push(...splitCsv(settings.sandboxEnvAllowlist));
1201
+ return uniqueEnvNames(names, "sandbox env");
1202
+ }
1203
+ function sandboxLifecycleHookIds(settings) {
1204
+ const ids = [];
1205
+ for (const profile of sandboxPreparationProfileNames(settings)) {
1206
+ ids.push(...sandboxPreparationProfiles[profile].hooks);
1207
+ }
1208
+ return uniqueValues(ids);
1209
+ }
1210
+ function sandboxPreparationProfileNames(settings) {
1211
+ const profiles = splitCsv(settings.sandboxPreparationProfiles).map((value) => value.toLowerCase());
1212
+ if (profiles.includes("none")) {
1213
+ if (profiles.length > 1) {
1214
+ throw new Error("OPENGENI_SANDBOX_PREPARATION_PROFILES cannot combine none with other profiles");
1215
+ }
1216
+ return ["none"];
1217
+ }
1218
+ for (const profile of profiles) {
1219
+ if (!sandboxPreparationProfiles[profile]) {
1220
+ throw new Error(`Unknown sandbox preparation profile ${profile}`);
1221
+ }
1222
+ }
1223
+ return profiles;
1224
+ }
1225
+ function parseExposedPorts(raw) {
1226
+ return splitCsv(raw).map((value) => {
1227
+ const port = Number(value);
1228
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
1229
+ throw new Error("OPENGENI_DOCKER_EXPOSED_PORTS must contain TCP port numbers");
1230
+ }
1231
+ return port;
1232
+ });
1233
+ }
1234
+ function parseMcpServers(raw) {
1235
+ if (!raw) {
1236
+ return void 0;
1237
+ }
1238
+ try {
1239
+ const parsed = JSON.parse(raw);
1240
+ if (!Array.isArray(parsed)) {
1241
+ throw new Error("value must be a JSON array");
1242
+ }
1243
+ return parsed;
1244
+ } catch (error) {
1245
+ const message = error instanceof Error ? error.message : String(error);
1246
+ throw new Error(`OPENGENI_MCP_SERVERS must be a JSON array: ${message}`);
1247
+ }
1248
+ }
1249
+ function parseModelPricingJson(raw) {
1250
+ if (!raw.trim() || raw.trim() === "{}") {
1251
+ return {};
1252
+ }
1253
+ let parsed;
1254
+ try {
1255
+ parsed = JSON.parse(raw);
1256
+ } catch (error) {
1257
+ const message = error instanceof Error ? error.message : String(error);
1258
+ throw new Error(`OPENGENI_MODEL_PRICING_JSON must be valid JSON: ${message}`);
1259
+ }
1260
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1261
+ throw new Error("OPENGENI_MODEL_PRICING_JSON must be a JSON object keyed by model name");
1262
+ }
1263
+ const out = {};
1264
+ for (const [model, value] of Object.entries(parsed)) {
1265
+ if (!model.trim()) {
1266
+ throw new Error("OPENGENI_MODEL_PRICING_JSON contains an empty model name");
1267
+ }
1268
+ out[model] = ModelPricingSchema.parse(value);
1269
+ }
1270
+ return out;
1271
+ }
1272
+ function parseSandboxWarmRateJson(raw) {
1273
+ if (!raw.trim() || raw.trim() === "{}") {
1274
+ return {};
1275
+ }
1276
+ let parsed;
1277
+ try {
1278
+ parsed = JSON.parse(raw);
1279
+ } catch (error) {
1280
+ const message = error instanceof Error ? error.message : String(error);
1281
+ throw new Error(`OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON must be valid JSON: ${message}`);
1282
+ }
1283
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1284
+ throw new Error("OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON must be a JSON object keyed by backend name");
1285
+ }
1286
+ const out = {};
1287
+ for (const [backend, value] of Object.entries(parsed)) {
1288
+ if (!backend.trim()) {
1289
+ throw new Error("OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON contains an empty backend name");
1290
+ }
1291
+ const rate = typeof value === "number" ? value : Number(value);
1292
+ if (!Number.isFinite(rate) || rate < 0) {
1293
+ throw new Error(`OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON rate for ${backend} must be a non-negative number`);
1294
+ }
1295
+ out[backend] = rate;
1296
+ }
1297
+ return out;
1298
+ }
1299
+ function sandboxWarmRateMicrosPerSecond(settings, backend) {
1300
+ const table = parseSandboxWarmRateJson(settings.sandboxWarmRateMicrosPerSecondJson);
1301
+ return table[backend] ?? 0;
1302
+ }
1303
+ function parseModelProvidersJson(raw) {
1304
+ if (!raw.trim() || raw.trim() === "[]") {
1305
+ return [];
1306
+ }
1307
+ let parsed;
1308
+ try {
1309
+ parsed = JSON.parse(raw);
1310
+ } catch (error) {
1311
+ const message = error instanceof Error ? error.message : String(error);
1312
+ throw new Error(`OPENGENI_MODEL_PROVIDERS_JSON must be valid JSON: ${message}`);
1313
+ }
1314
+ if (!Array.isArray(parsed)) {
1315
+ throw new Error("OPENGENI_MODEL_PROVIDERS_JSON must be a JSON array of providers");
1316
+ }
1317
+ return parsed.map((entry, index) => {
1318
+ const result = RegistryProviderSchema.safeParse(entry);
1319
+ if (!result.success) {
1320
+ throw new Error(`OPENGENI_MODEL_PROVIDERS_JSON provider[${index}] is invalid: ${result.error.message}`);
1321
+ }
1322
+ return result.data;
1323
+ });
1324
+ }
1325
+ function parseStaticUsageLimitsJson(raw) {
1326
+ if (!raw.trim() || raw.trim() === "{}") {
1327
+ return {};
1328
+ }
1329
+ let parsed;
1330
+ try {
1331
+ parsed = JSON.parse(raw);
1332
+ } catch (error) {
1333
+ const message = error instanceof Error ? error.message : String(error);
1334
+ throw new Error(`OPENGENI_STATIC_USAGE_LIMITS_JSON must be valid JSON: ${message}`);
1335
+ }
1336
+ return StaticUsageLimits.parse(parsed);
1337
+ }
1338
+ function parseStaticEntitlementsJson(raw) {
1339
+ if (!raw.trim() || raw.trim() === "{}") {
1340
+ return {};
1341
+ }
1342
+ let parsed;
1343
+ try {
1344
+ parsed = JSON.parse(raw);
1345
+ } catch (error) {
1346
+ const message = error instanceof Error ? error.message : String(error);
1347
+ throw new Error(`OPENGENI_STATIC_ENTITLEMENTS_JSON must be valid JSON: ${message}`);
1348
+ }
1349
+ return Entitlements.parse(parsed);
1350
+ }
1351
+ function calculateEntryCostMicros(pricing, entry) {
1352
+ const inputTokens = positiveInt(entry.inputTokens);
1353
+ const outputTokens = positiveInt(entry.outputTokens);
1354
+ const cachedTokens = Math.min(inputTokens, cachedInputTokens(entry));
1355
+ const uncachedInputTokens = Math.max(0, inputTokens - cachedTokens);
1356
+ const cachedInputRate = pricing.cachedInputMicrosPerMillionTokens ?? pricing.inputMicrosPerMillionTokens;
1357
+ return Math.ceil(uncachedInputTokens * pricing.inputMicrosPerMillionTokens / 1e6) + Math.ceil(cachedTokens * cachedInputRate / 1e6) + Math.ceil(outputTokens * pricing.outputMicrosPerMillionTokens / 1e6);
1358
+ }
1359
+ function cachedInputTokens(entry) {
1360
+ const details = Array.isArray(entry.inputTokensDetails) ? entry.inputTokensDetails : entry.inputTokensDetails ? [entry.inputTokensDetails] : [];
1361
+ let total = 0;
1362
+ for (const detail of details) {
1363
+ total += positiveInt(detail.cached_tokens) + positiveInt(detail.cachedInputTokens) + positiveInt(detail.cached_input_tokens);
1364
+ }
1365
+ return total;
1366
+ }
1367
+ function positiveInt(value) {
1368
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
1369
+ }
1370
+ function ensureBuiltInMcpServers(settings) {
1371
+ const existing = settings.mcpServers.filter((server) => server.id !== "opengeni");
1372
+ const firstPartyMcpUrl = firstPartyMcpServerUrl(settings);
1373
+ const firstPartyDocsMcpUrl = firstPartyDocumentsMcpServerUrl(firstPartyMcpUrl);
1374
+ const hasFiles = existing.some((server) => server.id === "files");
1375
+ const hasDocs = existing.some((server) => server.id === "docs");
1376
+ return [
1377
+ {
1378
+ id: "opengeni",
1379
+ name: "OpenGeni",
1380
+ url: firstPartyMcpUrl,
1381
+ // The opengeni server's tools/list response is permission-scoped: it
1382
+ // varies by the calling session's delegated grant (e.g. a manager
1383
+ // session sees sessions_*/environment_* tools that a worker session
1384
+ // does not). The OpenAI Agents SDK caches tools/list in a process-global
1385
+ // map keyed only by the MCP server name, which is identical for every
1386
+ // session in the worker process. Caching here would let the first
1387
+ // session to warm the cache dictate what every later session sees,
1388
+ // regardless of permissions. tools/list is a cheap per-turn call, so we
1389
+ // never cache it. (The files server pins allowedTools to a single
1390
+ // permission-invariant tool and docs is already uncached, so both stay
1391
+ // safe to cache / leave as-is.)
1392
+ cacheToolsList: false
1393
+ },
1394
+ ...hasFiles ? [] : [{
1395
+ id: "files",
1396
+ name: "Files",
1397
+ url: firstPartyMcpUrl,
1398
+ allowedTools: ["files_get_download_url"],
1399
+ cacheToolsList: true
1400
+ }],
1401
+ ...hasDocs ? [] : [{
1402
+ id: "docs",
1403
+ name: "Document Search",
1404
+ url: firstPartyDocsMcpUrl,
1405
+ allowedTools: ["search_documents", "fetch_document_chunk", "list_document_bases"],
1406
+ cacheToolsList: false
1407
+ }],
1408
+ ...existing
1409
+ ];
1410
+ }
1411
+ function firstPartyMcpBaseUrl(settings) {
1412
+ return settings.opengeniMcpUrl ?? `http://127.0.0.1:${settings.apiPort}/v1/workspaces/{workspaceId}/mcp`;
1413
+ }
1414
+ function firstPartyMcpServerUrl(settings) {
1415
+ return firstPartyMcpBaseUrl(settings);
1416
+ }
1417
+ function firstPartyDocumentsMcpServerUrl(mcpUrl) {
1418
+ return `${mcpUrl.replace(/\/+$/, "")}/docs`;
1419
+ }
1420
+ function validateSettings(settings) {
1421
+ if (settings.productAccessMode === "managed") {
1422
+ if (!settings.publicBaseUrl) {
1423
+ throw new Error("OPENGENI_PUBLIC_BASE_URL is required when OPENGENI_PRODUCT_ACCESS_MODE=managed");
1424
+ }
1425
+ if (!settings.betterAuthSecret) {
1426
+ throw new Error("OPENGENI_BETTER_AUTH_SECRET is required when OPENGENI_PRODUCT_ACCESS_MODE=managed");
1427
+ }
1428
+ if (!settings.delegationSecret) {
1429
+ throw new Error("OPENGENI_DELEGATION_SECRET is required when OPENGENI_PRODUCT_ACCESS_MODE=managed");
1430
+ }
1431
+ if (!["local", "test"].includes(settings.environment) && !settings.resendApiKey) {
1432
+ throw new Error("OPENGENI_RESEND_API_KEY is required for managed mode outside local/test");
1433
+ }
1434
+ if (!["local", "test"].includes(settings.environment) && !settings.environmentsEncryptionKey) {
1435
+ throw new Error("OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is required for managed mode outside local/test");
1436
+ }
1437
+ }
1438
+ environmentsEncryptionKeyBytes(settings);
1439
+ if (settings.productAccessMode === "configured" && !["local", "test"].includes(settings.environment) && !settings.delegationSecret && !settings.authRequired) {
1440
+ throw new Error("OPENGENI_PRODUCT_ACCESS_MODE=configured requires OPENGENI_DELEGATION_SECRET or OPENGENI_AUTH_REQUIRED=true outside local/test");
1441
+ }
1442
+ if (settings.billingMode === "stripe") {
1443
+ if (!settings.stripeSecretKey || !settings.stripeWebhookSecret) {
1444
+ throw new Error("OPENGENI_STRIPE_SECRET_KEY and OPENGENI_STRIPE_WEBHOOK_SECRET are required when OPENGENI_BILLING_MODE=stripe");
1445
+ }
1446
+ }
1447
+ if (settings.productAccessMode !== "managed" && settings.billingMode === "stripe") {
1448
+ throw new Error("OPENGENI_BILLING_MODE=stripe requires OPENGENI_PRODUCT_ACCESS_MODE=managed");
1449
+ }
1450
+ if (settings.billingMode === "stripe" || settings.usageLimitsMode === "managed") {
1451
+ const pricing = configuredModelPricing(settings);
1452
+ const missing = configuredAllowedModels(settings).filter((model) => !pricing[model]);
1453
+ if (missing.length > 0) {
1454
+ throw new Error(`Missing model pricing for managed billing model(s): ${missing.join(", ")}. Set OPENGENI_MODEL_PRICING_JSON.`);
1455
+ }
1456
+ }
1457
+ if (settings.usageLimitsMode === "static") {
1458
+ const limits = configuredStaticUsageLimits(settings);
1459
+ if (Object.keys(limits).length === 0) {
1460
+ throw new Error("OPENGENI_STATIC_USAGE_LIMITS_JSON must define at least one cap when OPENGENI_USAGE_LIMITS_MODE=static");
1461
+ }
1462
+ } else {
1463
+ parseStaticUsageLimitsJson(settings.staticUsageLimitsJson);
1464
+ }
1465
+ if (settings.entitlementsMode === "static") {
1466
+ const entitlements = parseStaticEntitlementsJson(settings.staticEntitlementsJson);
1467
+ if (Object.keys(entitlements).length === 0) {
1468
+ throw new Error("OPENGENI_STATIC_ENTITLEMENTS_JSON must define at least one feature when OPENGENI_ENTITLEMENTS_MODE=static");
1469
+ }
1470
+ } else {
1471
+ parseStaticEntitlementsJson(settings.staticEntitlementsJson);
1472
+ }
1473
+ if (settings.authRequired && !settings.accessKey) {
1474
+ throw new Error("OPENGENI_ACCESS_KEY is required when OPENGENI_AUTH_REQUIRED=true");
1475
+ }
1476
+ if (settings.openaiProvider === "azure") {
1477
+ if (!settings.azureOpenaiBaseUrl && !settings.azureOpenaiEndpoint) {
1478
+ throw new Error("Azure OpenAI requires OPENGENI_AZURE_OPENAI_BASE_URL or OPENGENI_AZURE_OPENAI_ENDPOINT");
1479
+ }
1480
+ if (!settings.azureOpenaiBaseUrl && !settings.azureOpenaiDeployment) {
1481
+ throw new Error("Azure OpenAI endpoint mode requires OPENGENI_AZURE_OPENAI_DEPLOYMENT");
1482
+ }
1483
+ if (!settings.azureOpenaiBaseUrl && !settings.azureOpenaiApiVersion) {
1484
+ throw new Error("Azure OpenAI endpoint mode requires OPENGENI_AZURE_OPENAI_API_VERSION");
1485
+ }
1486
+ if (!settings.azureOpenaiApiKey && !settings.azureOpenaiAdToken) {
1487
+ throw new Error("Azure OpenAI requires an API key or AD token");
1488
+ }
1489
+ }
1490
+ if (Boolean(settings.modalTokenId) !== Boolean(settings.modalTokenSecret)) {
1491
+ throw new Error("OPENGENI_MODAL_TOKEN_ID and OPENGENI_MODAL_TOKEN_SECRET must both be set or both omitted");
1492
+ }
1493
+ for (const required of SANDBOX_REQUIRED_ENV[settings.sandboxBackend] ?? []) {
1494
+ const value = settings[required.field];
1495
+ if (value === void 0 || value === null || typeof value === "string" && value.trim().length === 0) {
1496
+ throw new Error(`${required.env} is required when OPENGENI_SANDBOX_BACKEND=${settings.sandboxBackend}`);
1497
+ }
1498
+ }
1499
+ if (settings.objectStorageBackend === "s3-compatible" || settings.objectStorageBackend === "aws-s3") {
1500
+ if (Boolean(settings.objectStorageAccessKeyId) !== Boolean(settings.objectStorageSecretAccessKey)) {
1501
+ throw new Error("OPENGENI_OBJECT_STORAGE_ACCESS_KEY_ID and OPENGENI_OBJECT_STORAGE_SECRET_ACCESS_KEY must both be set or both omitted");
1502
+ }
1503
+ if (settings.objectStorageBackend === "s3-compatible" && (settings.objectStorageEndpoint || settings.objectStorageSandboxEndpoint) && (!settings.objectStorageAccessKeyId || !settings.objectStorageSecretAccessKey)) {
1504
+ throw new Error("S3-compatible object storage endpoints require OPENGENI_OBJECT_STORAGE_ACCESS_KEY_ID and OPENGENI_OBJECT_STORAGE_SECRET_ACCESS_KEY");
1505
+ }
1506
+ if (settings.objectStorageAzureConnectionString || settings.objectStorageAzureAccountName || settings.objectStorageAzureAccountKey || settings.objectStorageAzureEndpoint) {
1507
+ throw new Error("S3 object storage uses OPENGENI_OBJECT_STORAGE_* S3 settings, not OPENGENI_OBJECT_STORAGE_AZURE_* settings");
1508
+ }
1509
+ if (settings.objectStorageGcsProjectId || settings.objectStorageGcsCredentialsJson || settings.objectStorageGcsKeyFilename || settings.objectStorageGcsApiEndpoint) {
1510
+ throw new Error("S3 object storage uses OPENGENI_OBJECT_STORAGE_* S3 settings, not OPENGENI_OBJECT_STORAGE_GCS_* settings");
1511
+ }
1512
+ } else if (settings.objectStorageBackend === "azure-blob") {
1513
+ if (settings.objectStorageEndpoint || settings.objectStorageSandboxEndpoint || settings.objectStorageAccessKeyId || settings.objectStorageSecretAccessKey) {
1514
+ throw new Error("Azure Blob storage uses OPENGENI_OBJECT_STORAGE_AZURE_* settings, not S3-compatible object storage settings");
1515
+ }
1516
+ if (settings.objectStorageGcsProjectId || settings.objectStorageGcsCredentialsJson || settings.objectStorageGcsKeyFilename || settings.objectStorageGcsApiEndpoint) {
1517
+ throw new Error("Azure Blob storage uses OPENGENI_OBJECT_STORAGE_AZURE_* settings, not OPENGENI_OBJECT_STORAGE_GCS_* settings");
1518
+ }
1519
+ const hasConnectionString = Boolean(settings.objectStorageAzureConnectionString);
1520
+ const hasSharedKey = Boolean(settings.objectStorageAzureAccountName) && Boolean(settings.objectStorageAzureAccountKey);
1521
+ if (!hasConnectionString && !hasSharedKey) {
1522
+ throw new Error("Azure Blob storage requires OPENGENI_OBJECT_STORAGE_AZURE_CONNECTION_STRING or OPENGENI_OBJECT_STORAGE_AZURE_ACCOUNT_NAME plus OPENGENI_OBJECT_STORAGE_AZURE_ACCOUNT_KEY");
1523
+ }
1524
+ } else {
1525
+ if (settings.objectStorageEndpoint || settings.objectStorageSandboxEndpoint || settings.objectStorageAccessKeyId || settings.objectStorageSecretAccessKey) {
1526
+ throw new Error("GCS object storage uses OPENGENI_OBJECT_STORAGE_GCS_* settings, not S3-compatible object storage settings");
1527
+ }
1528
+ if (settings.objectStorageAzureConnectionString || settings.objectStorageAzureAccountName || settings.objectStorageAzureAccountKey || settings.objectStorageAzureEndpoint) {
1529
+ throw new Error("GCS object storage uses OPENGENI_OBJECT_STORAGE_GCS_* settings, not OPENGENI_OBJECT_STORAGE_AZURE_* settings");
1530
+ }
1531
+ if (settings.objectStorageGcsCredentialsJson) {
1532
+ parseGcsCredentialsJson(settings.objectStorageGcsCredentialsJson);
1533
+ }
1534
+ }
1535
+ if (settings.documentChunkOverlap >= settings.documentChunkSize) {
1536
+ throw new Error("OPENGENI_DOCUMENT_CHUNK_OVERLAP must be smaller than OPENGENI_DOCUMENT_CHUNK_SIZE");
1537
+ }
1538
+ parseExposedPorts(settings.dockerExposedPorts);
1539
+ sandboxEnvironmentVariableNames(settings);
1540
+ sandboxLifecycleHookIds(settings);
1541
+ parseSandboxWarmRateJson(settings.sandboxWarmRateMicrosPerSecondJson);
1542
+ const serverIds = /* @__PURE__ */ new Set();
1543
+ for (const server of settings.mcpServers) {
1544
+ if (serverIds.has(server.id)) {
1545
+ throw new Error(`OPENGENI_MCP_SERVERS contains duplicate id ${server.id}`);
1546
+ }
1547
+ serverIds.add(server.id);
1548
+ }
1549
+ {
1550
+ const reaperPeriod = settings.sandboxLeaseReaperPeriodMs;
1551
+ const viewerTtl = settings.sandboxViewerHolderTtlMs;
1552
+ const idleGraceMs = settings.sandboxIdleGraceMs;
1553
+ const providerLifetimeMs = settings.modalTimeoutSeconds * 1e3;
1554
+ const idleTimeoutMs = effectiveModalIdleTimeoutSeconds(settings) * 1e3;
1555
+ if (!(reaperPeriod < viewerTtl)) {
1556
+ throw new Error(
1557
+ `OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS (${reaperPeriod}) must be strictly less than OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS (${viewerTtl}): the reaper must run more often than the TTL it polices, or stale viewer holders outlive a full reaper period.`
1558
+ );
1559
+ }
1560
+ if (!(idleTimeoutMs <= providerLifetimeMs)) {
1561
+ throw new Error(
1562
+ `OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS*1000 (${idleTimeoutMs}) must not exceed the hard provider lifetime (OPENGENI_MODAL_TIMEOUT_SECONDS*1000 = ${providerLifetimeMs}): the idle timeout is a floor under the hard lifetime, not above it.`
1563
+ );
1564
+ }
1565
+ if (!(viewerTtl < idleTimeoutMs)) {
1566
+ throw new Error(
1567
+ `OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS (${viewerTtl}) must be strictly less than the effective box idle timeout (${idleTimeoutMs}): a viewer holder must be reapable before the box idles out from under it (the provider idle-timeout is the backstop).`
1568
+ );
1569
+ }
1570
+ if (!(reaperPeriod + idleGraceMs < idleTimeoutMs)) {
1571
+ throw new Error(
1572
+ `OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS + OPENGENI_SANDBOX_IDLE_GRACE_MS (${reaperPeriod} + ${idleGraceMs} = ${reaperPeriod + idleGraceMs}) must be strictly less than the effective box idle timeout (${idleTimeoutMs}): a drained box must SURVIVE its full warm window so the reaper can resume + snapshot /workspace + terminate it on the sweep AFTER the drain grace elapses \u2014 Modal's idle-reap must NOT fire first (or /workspace is lost). Raise OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS (defaults to OPENGENI_MODAL_TIMEOUT_SECONDS) or lower OPENGENI_SANDBOX_IDLE_GRACE_MS.`
1573
+ );
1574
+ }
1575
+ }
1576
+ if (settings.sandboxDesktopEnabled && resolveStreamTokenSecret(settings) === void 0) {
1577
+ console.warn(
1578
+ "[opengeni] OPENGENI_SANDBOX_DESKTOP_ENABLED=true but neither OPENGENI_STREAM_TOKEN_SECRET nor OPENGENI_DELEGATION_SECRET is set: the desktop pixel plane will GRACEFULLY DEGRADE (DesktopStream.transport=null \u2014 no scoped stream tokens can be minted). Set OPENGENI_STREAM_TOKEN_SECRET to enable the live desktop stream."
1579
+ );
1580
+ }
1581
+ const registryProviders = parseModelProvidersJson(settings.modelProvidersJson);
1582
+ const builtinId = builtinProviderId(settings);
1583
+ const providerIds = /* @__PURE__ */ new Set();
1584
+ for (const provider of registryProviders) {
1585
+ if (provider.id === builtinId) {
1586
+ throw new Error(`OPENGENI_MODEL_PROVIDERS_JSON provider id ${provider.id} collides with the built-in provider id`);
1587
+ }
1588
+ if (providerIds.has(provider.id)) {
1589
+ throw new Error(`OPENGENI_MODEL_PROVIDERS_JSON contains duplicate provider id ${provider.id}`);
1590
+ }
1591
+ providerIds.add(provider.id);
1592
+ if (!resolveProviderApiKey(provider)) {
1593
+ throw new Error(`OPENGENI_MODEL_PROVIDERS_JSON provider ${provider.id} requires a resolvable API key (set apiKey or apiKeyEnv)`);
1594
+ }
1595
+ }
1596
+ }
1597
+ function resolveStreamTokenSecret(settings) {
1598
+ const explicit = settings.streamTokenSecret?.trim();
1599
+ if (explicit) {
1600
+ return explicit;
1601
+ }
1602
+ const delegation = settings.delegationSecret?.trim();
1603
+ return delegation ? delegation : void 0;
1604
+ }
1605
+ function streamTokenDegraded(settings) {
1606
+ return settings.sandboxDesktopEnabled && resolveStreamTokenSecret(settings) === void 0;
1607
+ }
1608
+ function resolveEnrollmentSigningSecret(settings) {
1609
+ const explicit = settings.enrollmentSigningSecret?.trim();
1610
+ if (explicit) {
1611
+ return explicit;
1612
+ }
1613
+ const delegation = settings.delegationSecret?.trim();
1614
+ return delegation ? delegation : void 0;
1615
+ }
1616
+ function resolveRelayTokenSecret(settings) {
1617
+ const explicit = settings.selfhostedRelayTokenSecret?.trim();
1618
+ if (explicit) {
1619
+ return explicit;
1620
+ }
1621
+ const stream = settings.streamTokenSecret?.trim();
1622
+ if (stream) {
1623
+ return stream;
1624
+ }
1625
+ const delegation = settings.delegationSecret?.trim();
1626
+ return delegation ? delegation : void 0;
1627
+ }
1628
+ function resolveNatsCalloutConfig(settings) {
1629
+ const accountSeed = settings.selfhostedNatsCalloutAccountSeed?.trim();
1630
+ const accountName = settings.selfhostedNatsCalloutAccountName?.trim() || "APP";
1631
+ const user = settings.selfhostedNatsCalloutUser?.trim();
1632
+ const password = settings.selfhostedNatsCalloutPassword?.trim();
1633
+ if (!accountSeed || !user || !password) {
1634
+ return null;
1635
+ }
1636
+ return { accountSeed, accountName, user, password };
1637
+ }
1638
+ function resolveNatsControlPlaneAuth(settings) {
1639
+ const user = settings.selfhostedNatsControlUser?.trim();
1640
+ const password = settings.selfhostedNatsControlPassword?.trim();
1641
+ if (!user || !password) {
1642
+ return null;
1643
+ }
1644
+ return { user, password };
1645
+ }
1646
+ function splitCsv(raw) {
1647
+ return raw.split(",").map((value) => value.trim()).filter(Boolean);
1648
+ }
1649
+ function uniqueEnvNames(raw, fieldName) {
1650
+ const seen = /* @__PURE__ */ new Set();
1651
+ const out = [];
1652
+ for (const name of raw) {
1653
+ if (!envName.test(name)) {
1654
+ throw new Error(`${fieldName} contains invalid variable name ${name}`);
1655
+ }
1656
+ if (!seen.has(name)) {
1657
+ seen.add(name);
1658
+ out.push(name);
1659
+ }
1660
+ }
1661
+ return out;
1662
+ }
1663
+ function uniqueValues(raw) {
1664
+ return [...new Set(raw.filter(Boolean))];
1665
+ }
1666
+ function parseGcsCredentialsJson(raw) {
1667
+ try {
1668
+ return JSON.parse(raw);
1669
+ } catch (error) {
1670
+ const message = error instanceof Error ? error.message : String(error);
1671
+ throw new Error(`OPENGENI_OBJECT_STORAGE_GCS_CREDENTIALS_JSON must be valid JSON: ${message}`);
1672
+ }
1673
+ }
1674
+ function delay(ms) {
1675
+ return new Promise((resolve) => setTimeout(resolve, ms));
1676
+ }
1677
+ export {
1678
+ AGENT_INSTRUCTIONS_CORE_PLACEHOLDER,
1679
+ DEFAULT_AGENT_INSTRUCTIONS,
1680
+ ModelProviderApi,
1681
+ RegistryProviderKind,
1682
+ SANDBOX_REQUIRED_ENV,
1683
+ applyGitAuthPointerEnvironment,
1684
+ calculateModelUsageCostMicros,
1685
+ collectGitIdentityEnvironment,
1686
+ collectSandboxEnvironment,
1687
+ configuredAllowedModels,
1688
+ configuredAllowedReasoningEfforts,
1689
+ configuredEntitlements,
1690
+ configuredModelPricing,
1691
+ configuredModels,
1692
+ configuredProviders,
1693
+ configuredStaticUsageLimits,
1694
+ contextInputBudgetTokens,
1695
+ contextServerCompactThreshold,
1696
+ dbSearchPath,
1697
+ defaultModelPricing,
1698
+ effectiveModalIdleTimeoutSeconds,
1699
+ environmentsEncryptionKeyBytes,
1700
+ firstPartyMcpBaseUrl,
1701
+ getSettings,
1702
+ hasGitHubRepositorySelection,
1703
+ parseExposedPorts,
1704
+ parseMcpServers,
1705
+ parseModelPricingJson,
1706
+ parseModelProvidersJson,
1707
+ parseSandboxWarmRateJson,
1708
+ parseStaticEntitlementsJson,
1709
+ parseStaticUsageLimitsJson,
1710
+ requiredSandboxEnvForBackend,
1711
+ resolveContextCompactionMode,
1712
+ resolveEnrollmentSigningSecret,
1713
+ resolveModelProvider,
1714
+ resolveNatsCalloutConfig,
1715
+ resolveNatsControlPlaneAuth,
1716
+ resolveProviderApiKey,
1717
+ resolveRelayTokenSecret,
1718
+ resolveStreamTokenSecret,
1719
+ retryStartupDependency,
1720
+ sandboxEnvironmentVariableNames,
1721
+ sandboxLifecycleHookIds,
1722
+ sandboxPreparationProfiles,
1723
+ sandboxWarmRateMicrosPerSecond,
1724
+ stableSandboxEnvironmentForRun,
1725
+ startupRetryOptions,
1726
+ streamTokenDegraded
1727
+ };
1728
+ //# sourceMappingURL=index.js.map