@opengeni/config 0.3.0 → 0.5.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.d.ts +79 -58
- package/dist/index.js +425 -204
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
- package/src/index.ts +725 -340
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
StaticUsageLimits,
|
|
10
10
|
UsageLimitsMode,
|
|
11
11
|
} from "@opengeni/contracts";
|
|
12
|
-
import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex/constants";
|
|
12
|
+
import { CODEX_MODEL_ID_PREFIX, CODEX_PROVIDER_ID } from "@opengeni/codex/constants";
|
|
13
13
|
import { z } from "zod";
|
|
14
14
|
|
|
15
15
|
const envName = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -81,10 +81,10 @@ export const AGENT_INSTRUCTIONS_CORE_PLACEHOLDER = "{{core}}";
|
|
|
81
81
|
* baked into this overridable string.
|
|
82
82
|
*
|
|
83
83
|
* INVARIANT: with no per-workspace override and an empty environment, the
|
|
84
|
-
* runtime's composed instructions are
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
84
|
+
* runtime's composed instructions are byte-for-byte pinned by a runtime test.
|
|
85
|
+
* The template below is joined by " ", followed by " " + the placeholder.
|
|
86
|
+
* Changing a single character here changes that default; update the pin
|
|
87
|
+
* intentionally.
|
|
88
88
|
*/
|
|
89
89
|
export const DEFAULT_AGENT_INSTRUCTIONS = [
|
|
90
90
|
"You are an OpenGeni workspace agent.",
|
|
@@ -94,21 +94,23 @@ export const DEFAULT_AGENT_INSTRUCTIONS = [
|
|
|
94
94
|
"File resources are mounted under files/<file-id>/ unless the session specifies another mount path.",
|
|
95
95
|
"Attached files are mounted read-only; copy them before modifying.",
|
|
96
96
|
"Bundled skills are under .agents/ and can include infrastructure, marketing, or other role-specific guidance.",
|
|
97
|
-
"Use Checkov, Terraform, Azure CLI,
|
|
97
|
+
"Use Checkov, Terraform, Azure CLI, git provider CLIs, and repository tools when relevant; gh, glab, and az repos are pre-authenticated when the host brokers matching git credentials.",
|
|
98
98
|
"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.",
|
|
99
|
-
"Treat code-changing work as GitOps work: create a focused branch/commit/PR when
|
|
99
|
+
"Treat code-changing work as GitOps work: create a focused branch/commit/PR when git provider credentials are available; otherwise report exact commands and blockers.",
|
|
100
100
|
"Return concise, factual summaries with files changed, commands run, and remaining blockers.",
|
|
101
101
|
AGENT_INSTRUCTIONS_CORE_PLACEHOLDER,
|
|
102
102
|
].join(" ");
|
|
103
103
|
|
|
104
|
-
export const McpServerConnectionRefSchema = z
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
104
|
+
export const McpServerConnectionRefSchema = z
|
|
105
|
+
.object({
|
|
106
|
+
connectionId: z.string().uuid().optional(),
|
|
107
|
+
providerDomain: z.string().min(1),
|
|
108
|
+
kind: z.enum(["oauth2", "api_key", "app_install", "delegated"]).optional(),
|
|
109
|
+
scopes: z.array(z.string().min(1)).optional(),
|
|
110
|
+
resource: z.string().min(1).optional(),
|
|
111
|
+
subjectScope: z.enum(["workspace", "subject"]).optional(),
|
|
112
|
+
})
|
|
113
|
+
.strict();
|
|
112
114
|
export type McpServerConnectionRef = z.infer<typeof McpServerConnectionRefSchema>;
|
|
113
115
|
|
|
114
116
|
const SettingsSchema = z.object({
|
|
@@ -144,7 +146,10 @@ const SettingsSchema = z.object({
|
|
|
144
146
|
// Base URL for the bring-your-own-compute agent release assets the get.<domain>
|
|
145
147
|
// install routes redirect to. Defaults to this repo's GitHub Releases. The route
|
|
146
148
|
// appends `/download/agent-v<ver>/<asset>` (or `/latest/download/<asset>`).
|
|
147
|
-
agentReleasesBaseUrl: z
|
|
149
|
+
agentReleasesBaseUrl: z
|
|
150
|
+
.string()
|
|
151
|
+
.url()
|
|
152
|
+
.default("https://github.com/Cloudgeni-ai/opengeni/releases"),
|
|
148
153
|
productAccessMode: ProductAccessMode.default("local"),
|
|
149
154
|
billingMode: BillingMode.default("disabled"),
|
|
150
155
|
entitlementsMode: EntitlementsMode.default("none"),
|
|
@@ -176,62 +181,45 @@ const SettingsSchema = z.object({
|
|
|
176
181
|
// it then acts as a hard ceiling that per-goal overrides can only lower.
|
|
177
182
|
goalMaxAutoContinuations: z.coerce.number().int().positive().optional(),
|
|
178
183
|
goalNoProgressLimit: z.coerce.number().int().positive().default(3),
|
|
184
|
+
// Temporary safety valve: spawned child sessions still run and retain their
|
|
185
|
+
// own durable events/goals, but terminal child episodes do not create parent
|
|
186
|
+
// system updates or inference work unless explicitly enabled.
|
|
187
|
+
childCompletionParentWakeEnabled: EnvBoolean.default(false),
|
|
179
188
|
// Per-segment ceiling on agent loop turns (model calls) within a single
|
|
180
189
|
// session turn. Effectively unbounded by default for the same reason as
|
|
181
190
|
// above; the graceful max-turns valve (idle + goal continuation, never a
|
|
182
191
|
// session failure) remains as inert safety should a deployment set a cap.
|
|
183
192
|
agentMaxModelCallsPerTurn: z.coerce.number().int().positive().default(1_000_000),
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// RunState blob. Items and the sandbox envelope are dual-written
|
|
188
|
-
// unconditionally; this flag governs the read path only, so flipping back to
|
|
189
|
-
// "run_state" remains a safe rollback at any time.
|
|
190
|
-
sessionHistorySource: z.enum(["run_state", "items"]).default("items"),
|
|
191
|
-
// Provider-aware conversation context management (long-lived sessions
|
|
192
|
-
// otherwise grow unbounded until they overflow the model context window and
|
|
193
|
-
// hard-fail every turn). Resolution (see resolveContextCompactionMode):
|
|
194
|
-
// "auto" (default) -> "server" when openaiProvider === "openai" (the
|
|
195
|
-
// OpenAI platform Responses API honors server-side context_management),
|
|
196
|
-
// else "client" (Azure rejects context_management with a 400, so we run
|
|
197
|
-
// our own client-side compaction).
|
|
198
|
-
// "server" / "client" -> force that path regardless of provider.
|
|
199
|
-
// "off" -> neither path (legacy unbounded growth; escape hatch only).
|
|
200
|
-
contextCompactionMode: z.enum(["auto", "server", "client", "off"]).default("auto"),
|
|
201
|
-
// The model's real context window in tokens. gpt-5.5's true window is
|
|
202
|
-
// 1,050,000; it is absent from the SDK's hardcoded compaction window map (it
|
|
203
|
-
// knows only up to gpt-5.4), so the SDK's DynamicCompactionPolicy would fall
|
|
204
|
-
// back to a wrong 240k. We pass an explicit StaticCompactionPolicy threshold
|
|
205
|
-
// derived from these settings on the server path, and use the same numbers to
|
|
206
|
-
// budget the client path.
|
|
193
|
+
// The model family's real context window in tokens. OpenGeni always performs
|
|
194
|
+
// one durable, portable plaintext compaction transition; there is no
|
|
195
|
+
// provider/server/off mode ladder.
|
|
207
196
|
contextWindowTokens: z.coerce.number().int().positive().default(1_050_000),
|
|
197
|
+
// Optional model-catalog effective input ceiling. Codex models expose this as
|
|
198
|
+
// raw context_window * effective_context_window_percent; when absent, retain
|
|
199
|
+
// the deployment-level window-minus-reserved-output behavior.
|
|
200
|
+
contextEffectiveWindowTokens: z.coerce.number().int().positive().optional(),
|
|
208
201
|
// Proactive compaction threshold as a ratio of the model context window.
|
|
209
|
-
// Defaults to
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
202
|
+
// Defaults to 90%: compact as late as possible — retained context beats early
|
|
203
|
+
// headroom now that per-model windows are declared honestly (input-effective,
|
|
204
|
+
// empirically measured), and the fail-closed reactive compact-on-reject path
|
|
205
|
+
// absorbs any overshoot as one retried call rather than a dead session.
|
|
206
|
+
// Clamped to [0.3, 0.9] so deployments can tune the trigger without
|
|
207
|
+
// accidentally disabling compaction.
|
|
208
|
+
contextCompactionThresholdRatio: z.coerce
|
|
209
|
+
.number()
|
|
210
|
+
.default(0.9)
|
|
211
|
+
.transform((value) => {
|
|
212
|
+
if (!Number.isFinite(value)) {
|
|
213
|
+
return 0.9;
|
|
214
|
+
}
|
|
215
|
+
return Math.min(0.9, Math.max(0.3, value));
|
|
216
|
+
}),
|
|
218
217
|
// Tokens reserved for model output; subtracted from the window to get the
|
|
219
218
|
// usable input budget B = contextWindowTokens - contextReservedOutputTokens.
|
|
220
219
|
contextReservedOutputTokens: z.coerce.number().int().nonnegative().default(128_000),
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
contextServerCompactThresholdTokens: z.coerce.number().int().positive().optional(),
|
|
225
|
-
// Deprecated back-compat knobs. The threshold is now controlled by
|
|
226
|
-
// contextCompactionThresholdRatio; these remain parsed so older deployments do
|
|
227
|
-
// not fail boot when their env still contains them.
|
|
228
|
-
contextCompactSoftFraction: z.coerce.number().positive().max(1).default(0.70),
|
|
229
|
-
contextCompactHardFraction: z.coerce.number().positive().max(1).default(0.85),
|
|
230
|
-
// Deprecated for the client path; parsed for env/back-compat only.
|
|
231
|
-
contextKeepRecentTokens: z.coerce.number().int().positive().default(32_000),
|
|
232
|
-
// Parsed for back-compat. Client compaction uses the fixed 20k Codex summary
|
|
233
|
-
// buffer as its generated-summary output ceiling.
|
|
234
|
-
contextSummaryMaxTokens: z.coerce.number().int().positive().default(20_000),
|
|
220
|
+
// Model-catalog auto-compact limit. When present it is clamped to
|
|
221
|
+
// 90% of the raw window, matching Codex core's auto_compact_token_limit().
|
|
222
|
+
contextAutoCompactThresholdTokens: z.coerce.number().int().positive().optional(),
|
|
235
223
|
authRequired: EnvBoolean.default(false),
|
|
236
224
|
accessKey: z.string().optional(),
|
|
237
225
|
authAllowHealth: EnvBoolean.default(true),
|
|
@@ -244,8 +232,8 @@ const SettingsSchema = z.object({
|
|
|
244
232
|
openaiProvider: z.enum(["openai", "azure"]).default("openai"),
|
|
245
233
|
openaiApiKey: z.string().optional(),
|
|
246
234
|
openaiBaseUrl: z.string().optional(),
|
|
247
|
-
openaiModel: z.string().default("gpt-5.
|
|
248
|
-
openaiAllowedModels: z.string().default("gpt-5.
|
|
235
|
+
openaiModel: z.string().default("gpt-5.6-sol"),
|
|
236
|
+
openaiAllowedModels: z.string().default("gpt-5.6-sol,gpt-5.6-terra,gpt-5.6-luna"),
|
|
249
237
|
modelPricingJson: z.string().default("{}"),
|
|
250
238
|
// Extra (non-built-in) model providers, declared by the host as a JSON
|
|
251
239
|
// provider registry. Each entry carries its own base URL, API key, wire API
|
|
@@ -257,14 +245,19 @@ const SettingsSchema = z.object({
|
|
|
257
245
|
// Codex (ChatGPT) subscription: when enabled, a per-workspace connected
|
|
258
246
|
// subscription is injected as a synthetic "codex-subscription" registry
|
|
259
247
|
// provider whose models route through the ChatGPT backend (@opengeni/codex).
|
|
260
|
-
codexSubscriptionEnabled: EnvBoolean.default(false),
|
|
261
|
-
codexProductSku: z.string().optional(),
|
|
248
|
+
codexSubscriptionEnabled: EnvBoolean.default(false), // OPENGENI_CODEX_SUBSCRIPTION_ENABLED
|
|
249
|
+
codexProductSku: z.string().optional(), // OPENGENI_CODEX_PRODUCT_SKU (X-OpenAI-Product-Sku, apps only)
|
|
262
250
|
// Progressive connector disclosure (Codex-CLI-style tool_search): on a codex
|
|
263
251
|
// turn, flag the ~217 codex_apps connector tools `defer_loading:true` (dropping
|
|
264
252
|
// their schemas from model context) and add one client-executed tool_search
|
|
265
253
|
// tool that BM25-discloses only the matching connectors. Default OFF — a codex
|
|
266
254
|
// turn is byte-for-byte unchanged until enabled. OPENGENI_CODEX_TOOL_SEARCH_ENABLED
|
|
267
255
|
codexToolSearchEnabled: EnvBoolean.default(false),
|
|
256
|
+
// OPE-21 atomic, workspace-local credential allocation. Default OFF is a
|
|
257
|
+
// deliberate rolling-deploy fence: migrate + roll every worker first, then
|
|
258
|
+
// enable. Turning it off restores the legacy sticky selector without a schema
|
|
259
|
+
// rollback; the additive lease table/cursor columns become inert.
|
|
260
|
+
codexCredentialLeasingEnabled: EnvBoolean.default(false),
|
|
268
261
|
// Multi-account P3 (auto-rotation): an account is "near exhaustion" — ineligible to be
|
|
269
262
|
// rotated TO — when EITHER usage window (5h/weekly) is at/over this percent. Default 90 to
|
|
270
263
|
// match the UI danger flip (UsageBar danger at pct >= 90). OPENGENI_CODEX_ROTATION_NEAR_EXHAUSTION_PCT.
|
|
@@ -302,7 +295,7 @@ const SettingsSchema = z.object({
|
|
|
302
295
|
// the non-bypassable CORE at AGENT_INSTRUCTIONS_CORE_PLACEHOLDER (or appends
|
|
303
296
|
// it when the template omits the marker), and uses the result as the agent's
|
|
304
297
|
// instructions. Defaulting to DEFAULT_AGENT_INSTRUCTIONS keeps the composed
|
|
305
|
-
// default
|
|
298
|
+
// default pinned by runtime tests.
|
|
306
299
|
agentInstructionsTemplate: z.string().default(DEFAULT_AGENT_INSTRUCTIONS),
|
|
307
300
|
azureOpenaiBaseUrl: z.string().optional(),
|
|
308
301
|
azureOpenaiEndpoint: z.string().optional(),
|
|
@@ -317,6 +310,15 @@ const SettingsSchema = z.object({
|
|
|
317
310
|
dockerNetwork: z.string().optional(),
|
|
318
311
|
modalAppName: z.string().default("opengeni-sandbox"),
|
|
319
312
|
modalImageRef: z.string().optional(),
|
|
313
|
+
// Name of a Modal Secret (containing REGISTRY_USERNAME + REGISTRY_PASSWORD) used
|
|
314
|
+
// to authenticate the pull of `modalImageRef` from a PRIVATE registry. When UNSET
|
|
315
|
+
// (the default), the sandbox image is pulled UNAUTHENTICATED — i.e. it must be a
|
|
316
|
+
// PUBLIC registry tag, which is the only shape the Agents-extension Modal backend
|
|
317
|
+
// supports out of the box (`Image.fromRegistry(tag)` with no secret). Set this to
|
|
318
|
+
// run a private image (e.g. a cloud-hosted ACR/ECR/GCR digest): the runtime resolves
|
|
319
|
+
// the named Secret and builds the image via `fromRegistry(tag, secret)` before the
|
|
320
|
+
// first sandbox is created. Knob: OPENGENI_MODAL_IMAGE_REGISTRY_SECRET.
|
|
321
|
+
modalImageRegistrySecret: z.string().optional(),
|
|
320
322
|
// Modal's hard sandbox lifetime (timeoutMs = this * 1000), counted from each
|
|
321
323
|
// create/resume — it is the BACKSTOP that reclaims a box if the reaper/worker is
|
|
322
324
|
// down, NOT the warm-window controller (that's sandboxIdleGraceMs). It must
|
|
@@ -400,6 +402,13 @@ const SettingsSchema = z.object({
|
|
|
400
402
|
// recordingMaxSeconds is the ffmpeg -t hard ceiling (bounds a multi-day turn).
|
|
401
403
|
recordingEnabled: EnvBoolean.default(true),
|
|
402
404
|
recordingDefaultCodec: z.enum(["h264-mp4", "vp9-webm"]).default("h264-mp4"),
|
|
405
|
+
// Workbench v2 turn-end workspace capture (dossier §10.1). When on, the turn
|
|
406
|
+
// activity probes the box's changed files off the live box at turn end and
|
|
407
|
+
// persists a capture revision (blobs in @opengeni/storage) so the workbench
|
|
408
|
+
// paints cold/offline sessions with zero machine round-trips. Best-effort and
|
|
409
|
+
// fully behind this flag: off ⇒ capture is skipped and reads fall back to the
|
|
410
|
+
// live/wake path (status-quo behavior). Default on; explicit per environment.
|
|
411
|
+
workspaceCaptureEnabled: EnvBoolean.default(true),
|
|
403
412
|
recordingFramerate: z.coerce.number().int().positive().default(15),
|
|
404
413
|
recordingMaxSeconds: z.coerce.number().int().positive().default(600),
|
|
405
414
|
recordingMaxBytes: z.coerce.number().int().positive().default(268_435_456), // 256 MB
|
|
@@ -453,6 +462,19 @@ const SettingsSchema = z.object({
|
|
|
453
462
|
// EnvBoolean (NOT z.coerce.boolean(), which would coerce "false" -> true and
|
|
454
463
|
// turn the flag ON the moment anyone set the env var to disable it).
|
|
455
464
|
sandboxOwnershipEnabled: EnvBoolean.default(false),
|
|
465
|
+
// --- lazy sandbox provisioning rollout flag, default OFF ---
|
|
466
|
+
// Only effective when sandboxOwnershipEnabled is ALSO on (lazy provisioning is a
|
|
467
|
+
// property of the owned path — the SDK never creates/resumes an injected session,
|
|
468
|
+
// so we control when the box is established). When TRUE, a turn does NOT provision
|
|
469
|
+
// its box at turn start: the lease acquire + resume-by-id + hooks + downloads +
|
|
470
|
+
// heartbeat + recording are deferred to an in-process single-flight provisioner
|
|
471
|
+
// that runs the FIRST time a sandbox op is dispatched (via the routing proxy's
|
|
472
|
+
// resolveActiveBackend). A turn whose model never calls a sandbox-backed tool ends
|
|
473
|
+
// with NO lease row and ZERO warm-seconds. When FALSE (or ownership off) the turn
|
|
474
|
+
// provisions eagerly exactly as today — byte-for-byte. EnvBoolean (NOT
|
|
475
|
+
// z.coerce.boolean(), which coerces "false" -> true and would turn the flag ON the
|
|
476
|
+
// moment anyone set the env var to disable it).
|
|
477
|
+
sandboxLazyProvisionEnabled: EnvBoolean.default(false),
|
|
456
478
|
// --- bring-your-own-compute (selfhosted 11th backend) rollout flag, default OFF ---
|
|
457
479
|
// The keystone flag for the whole selfhosted feature (the enrollment device-flow,
|
|
458
480
|
// the NATS control plane, the relay stream tier). When FALSE the enrollment routes
|
|
@@ -461,6 +483,11 @@ const SettingsSchema = z.object({
|
|
|
461
483
|
// z.coerce.boolean(), which coerces "false" -> true). Flipped per-environment via
|
|
462
484
|
// the deploy-staging IaC secret/configmap pattern (dossier §17/§25.1).
|
|
463
485
|
sandboxSelfhostedEnabled: EnvBoolean.default(false),
|
|
486
|
+
// Gates the op-stream (streaming exec) transport to Connected Machines. The
|
|
487
|
+
// runner must ALSO advertise Capabilities.op_stream; default off, and legacy
|
|
488
|
+
// request/reply exec is the permanent fallback. EnvBoolean (NOT
|
|
489
|
+
// z.coerce.boolean(), which coerces "false" -> true).
|
|
490
|
+
agentOpStreamEnabled: EnvBoolean.default(false),
|
|
464
491
|
// The HMAC secret the control plane signs the enrollment bearer credential with
|
|
465
492
|
// (the `oge_` envelope the agent presents back to the control plane). Optional:
|
|
466
493
|
// when ABSENT and sandboxSelfhostedEnabled is on, the poll route reports the
|
|
@@ -517,6 +544,25 @@ const SettingsSchema = z.object({
|
|
|
517
544
|
// bus connects anonymously (local dev / a NATS with no auth_callout).
|
|
518
545
|
selfhostedNatsControlUser: z.string().optional(),
|
|
519
546
|
selfhostedNatsControlPassword: z.string().optional(),
|
|
547
|
+
// --- selfhosted (Connected Machine) control/exec op deadlines ---------------
|
|
548
|
+
// The control plane splits its op deadline in two. CONTROL ops (ping / fs / git /
|
|
549
|
+
// desktop / pty) must stay responsive so a machine's liveness is never masked by a
|
|
550
|
+
// slow op, so they use the short control timeout. EXEC gets its OWN, larger budget:
|
|
551
|
+
// a real command (compile, test run, dependency install) routinely outlives the
|
|
552
|
+
// control timeout, and before the split a long command was killed at the ~30s
|
|
553
|
+
// control wall. The agent kills the exec child at this deadline; the wire waits
|
|
554
|
+
// slightly longer (SELFHOSTED_EXEC_REPLY_GRACE_MS) for the typed timed-out reply.
|
|
555
|
+
//
|
|
556
|
+
// The exec default is a DELIBERATELY MODEST 2min (not 5): the agent-side admission
|
|
557
|
+
// pool is (until a later agent release) a FLAT 8 permits with no per-class split,
|
|
558
|
+
// so 8 slow execs holding a permit for 5 minutes would blanket-DRAIN every fs/git
|
|
559
|
+
// op — shipping the amplifier before the class-aware-admission fix. 2min still
|
|
560
|
+
// clears the large majority of the observed >30s exec tail; genuinely long jobs run
|
|
561
|
+
// in the background (see the exec-deadline hint) or raise the knob per deployment.
|
|
562
|
+
// Knobs: OPENGENI_SANDBOX_SELFHOSTED_EXEC_TIMEOUT_MS (default 2min) and
|
|
563
|
+
// OPENGENI_SANDBOX_SELFHOSTED_CONTROL_TIMEOUT_MS (default 30s).
|
|
564
|
+
sandboxSelfhostedExecTimeoutMs: z.coerce.number().int().positive().default(120_000),
|
|
565
|
+
sandboxSelfhostedControlTimeoutMs: z.coerce.number().int().positive().default(30_000),
|
|
520
566
|
// --- sandbox lease cadences (cadence invariant validated at boot below) ---
|
|
521
567
|
// reaperPeriod < viewerHolderTTL, and reaperPeriod + idleGrace < the EFFECTIVE
|
|
522
568
|
// box idle timeout (effectiveModalIdleTimeoutSeconds, which defaults to the hard
|
|
@@ -535,6 +581,22 @@ const SettingsSchema = z.object({
|
|
|
535
581
|
// fresh EMPTY box; lower it to trade warm cost for a snappier reclaim. Knob:
|
|
536
582
|
// OPENGENI_SANDBOX_IDLE_GRACE_MS.
|
|
537
583
|
sandboxIdleGraceMs: z.coerce.number().int().positive().default(900_000),
|
|
584
|
+
// MID-SESSION /workspace snapshot cadence (sandbox-file-persistence). The
|
|
585
|
+
// reaper's drain-persist only protects boxes the reaper itself kills; a box
|
|
586
|
+
// that dies any other way (Modal's hard creation-time timeout on a session
|
|
587
|
+
// busy past it, provider OOM/infra death) loses everything since the last
|
|
588
|
+
// clean drain. While a turn holds the box, the turn heartbeat and turn-end
|
|
589
|
+
// both take a snapshot when at least this interval has passed since the last
|
|
590
|
+
// one (same epoch-fenced fold-onto-lease seam as the drain), bounding the
|
|
591
|
+
// worst-case loss of ANY unclean box death to this window. 0 disables.
|
|
592
|
+
// Knob: OPENGENI_SANDBOX_SNAPSHOT_INTERVAL_MS. Default 15min.
|
|
593
|
+
sandboxSnapshotIntervalMs: z.coerce.number().int().min(0).default(900_000),
|
|
594
|
+
// Maximum time a best-effort /workspace snapshot capture may hold turn/reaper
|
|
595
|
+
// cleanup. A hung provider snapshot must never pin a lease holder, block
|
|
596
|
+
// graceful shutdown, or become permission to GC an older archive. Timeout is
|
|
597
|
+
// treated exactly like a failed best-effort snapshot. Knob:
|
|
598
|
+
// OPENGENI_SANDBOX_SNAPSHOT_TIMEOUT_MS. Default 60s.
|
|
599
|
+
sandboxSnapshotTimeoutMs: z.coerce.number().int().positive().default(60_000),
|
|
538
600
|
// expires_at refresh window for a held lease (>> the turn 10s heartbeat so a
|
|
539
601
|
// single missed heartbeat never TTL-reaps a live turn). The warming TTL is the
|
|
540
602
|
// window a cold->warming spawner has to commit warm before a reaper resets it.
|
|
@@ -544,6 +606,11 @@ const SettingsSchema = z.object({
|
|
|
544
606
|
// (a liveness/reaper cadence), this bounds how long one turn waits for capacity
|
|
545
607
|
// or provider creation before surfacing a clear turn.failed error.
|
|
546
608
|
sandboxWarmingTimeoutMs: z.coerce.number().int().positive().default(600_000),
|
|
609
|
+
// Rig setup-script budget (M3): the wall-clock timeout the rig-setup lifecycle
|
|
610
|
+
// hook runs its script under, distinct from the 120s per-command lifecycle
|
|
611
|
+
// default (a rig may compile/install heavy tooling on first cold create).
|
|
612
|
+
// Env: OPENGENI_RIG_SETUP_TIMEOUT_MS. Default 10min.
|
|
613
|
+
rigSetupTimeoutMs: z.coerce.number().int().positive().default(600_000),
|
|
547
614
|
// --- sandbox warm-time billing (P2.1) ---
|
|
548
615
|
// Per-backend warm rate (usd_micros/sec), like modelPricingJson: an empty {}
|
|
549
616
|
// means warm-cost is not debited (warm-seconds are still metered for audit).
|
|
@@ -559,7 +626,9 @@ const SettingsSchema = z.object({
|
|
|
559
626
|
sandboxEnvAllowlist: z.string().default(""),
|
|
560
627
|
objectStorageEndpoint: z.string().url().optional(),
|
|
561
628
|
objectStorageSandboxEndpoint: z.string().url().optional(),
|
|
562
|
-
objectStorageBackend: z
|
|
629
|
+
objectStorageBackend: z
|
|
630
|
+
.enum(["s3-compatible", "aws-s3", "azure-blob", "gcs"])
|
|
631
|
+
.default("s3-compatible"),
|
|
563
632
|
objectStorageBucket: z.string().min(1).default("opengeni-files"),
|
|
564
633
|
objectStorageRegion: z.string().min(1).default("us-east-1"),
|
|
565
634
|
objectStorageS3Provider: z.string().min(1).default("Minio"),
|
|
@@ -604,30 +673,34 @@ const SettingsSchema = z.object({
|
|
|
604
673
|
stripePublishableKey: z.string().optional(),
|
|
605
674
|
stripeWebhookSecret: z.string().optional(),
|
|
606
675
|
stripeCreditsProductId: z.string().optional(),
|
|
607
|
-
mcpServers: z
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
676
|
+
mcpServers: z
|
|
677
|
+
.array(
|
|
678
|
+
z.object({
|
|
679
|
+
id: z.string().min(1).regex(registryId),
|
|
680
|
+
name: z.string().min(1).optional(),
|
|
681
|
+
url: z.string().url(),
|
|
682
|
+
allowedTools: z.array(z.string().min(1)).optional(),
|
|
683
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
684
|
+
cacheToolsList: z.boolean().default(false),
|
|
685
|
+
/**
|
|
686
|
+
* Human-approval policy for this server's tools, overlaid per-run from a
|
|
687
|
+
* session MCP server row (never from OPENGENI_MCP_SERVERS). `true` = all
|
|
688
|
+
* tools require approval; a string[] = only the listed UNPREFIXED tool
|
|
689
|
+
* names do; absent = auto-run (the historical default). Enforced in the
|
|
690
|
+
* runtime by attaching `needsApproval` to the matching MCP tools.
|
|
691
|
+
*/
|
|
692
|
+
requireApproval: z.union([z.boolean(), z.array(z.string().min(1))]).optional(),
|
|
693
|
+
/**
|
|
694
|
+
* Extra request headers sent to this MCP server (credential injection
|
|
695
|
+
* for workspace-enabled capability MCPs). Populated at runtime from
|
|
696
|
+
* encrypted capability-installation credentials; do not put secrets in
|
|
697
|
+
* OPENGENI_MCP_SERVERS.
|
|
698
|
+
*/
|
|
699
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
700
|
+
connectionRef: McpServerConnectionRefSchema.optional(),
|
|
701
|
+
}),
|
|
702
|
+
)
|
|
703
|
+
.default([]),
|
|
631
704
|
});
|
|
632
705
|
|
|
633
706
|
export type Settings = z.infer<typeof SettingsSchema>;
|
|
@@ -676,23 +749,25 @@ export type RegistryProviderKind = z.infer<typeof RegistryProviderKind>;
|
|
|
676
749
|
|
|
677
750
|
/** A single model exposed by a registry provider. */
|
|
678
751
|
const RegistryModelSchema = z.object({
|
|
679
|
-
id: z.string().min(1),
|
|
680
|
-
label: z.string().min(1).optional(),
|
|
752
|
+
id: z.string().min(1), // model id sent to the provider, e.g. "accounts/fireworks/models/glm-5p2"
|
|
753
|
+
label: z.string().min(1).optional(), // display name; defaults to id
|
|
681
754
|
contextWindowTokens: z.number().int().positive().optional(),
|
|
682
|
-
|
|
683
|
-
|
|
755
|
+
effectiveContextWindowTokens: z.number().int().positive().optional(),
|
|
756
|
+
autoCompactTokenLimit: z.number().int().positive().optional(),
|
|
757
|
+
reasoningEffort: z.boolean().optional(), // model accepts a reasoning-effort control
|
|
758
|
+
hostedWebSearch: z.boolean().optional(), // provider executes the hosted web_search tool for this model
|
|
684
759
|
pricing: ModelPricingSchema.optional(),
|
|
685
760
|
});
|
|
686
761
|
|
|
687
762
|
/** A non-built-in provider declared by the host via OPENGENI_MODEL_PROVIDERS_JSON. */
|
|
688
763
|
const RegistryProviderSchema = z.object({
|
|
689
|
-
kind: RegistryProviderKind.default("api-key"),
|
|
690
|
-
id: z.string().min(1).regex(registryId),
|
|
764
|
+
kind: RegistryProviderKind.default("api-key"), // "codex-subscription" => per-request token, no static key
|
|
765
|
+
id: z.string().min(1).regex(registryId), // stable provider id, e.g. "fireworks"
|
|
691
766
|
label: z.string().min(1).optional(),
|
|
692
767
|
api: ModelProviderApi.default("chat"),
|
|
693
768
|
baseUrl: z.string().url(),
|
|
694
|
-
apiKey: z.string().optional(),
|
|
695
|
-
apiKeyEnv: z.string().optional(),
|
|
769
|
+
apiKey: z.string().optional(), // inline key (pragmatic) ...
|
|
770
|
+
apiKeyEnv: z.string().optional(), // ... OR name of the env var holding the key (preferred)
|
|
696
771
|
defaultQuery: z.record(z.string(), z.string()).optional(),
|
|
697
772
|
defaultHeaders: z.record(z.string(), z.string()).optional(),
|
|
698
773
|
models: z.array(RegistryModelSchema).min(1),
|
|
@@ -702,28 +777,29 @@ export type RegistryProvider = z.infer<typeof RegistryProviderSchema>;
|
|
|
702
777
|
export const IntegrationOAuthClientConfigSchema = z.object({
|
|
703
778
|
clientId: z.string().min(1),
|
|
704
779
|
clientSecret: z.string().min(1).optional(),
|
|
705
|
-
tokenEndpointAuthMethod: z
|
|
780
|
+
tokenEndpointAuthMethod: z
|
|
781
|
+
.enum(["none", "client_secret_post", "client_secret_basic"])
|
|
782
|
+
.default("none"),
|
|
706
783
|
});
|
|
707
784
|
export type IntegrationOAuthClientConfig = z.infer<typeof IntegrationOAuthClientConfigSchema>;
|
|
708
785
|
|
|
709
786
|
/**
|
|
710
787
|
* Runtime-resolved provider (built-in or registry), client-construction-ready.
|
|
711
788
|
* The built-in OpenAI/Azure provider is always present and always "responses";
|
|
712
|
-
* registry providers carry their own base URL / key / wire API.
|
|
713
|
-
*
|
|
714
|
-
*
|
|
789
|
+
* registry providers carry their own base URL / key / wire API. Compaction is
|
|
790
|
+
* not a provider capability: all providers use the same durable plaintext
|
|
791
|
+
* replacement.
|
|
715
792
|
*/
|
|
716
793
|
export interface ResolvedModelProvider {
|
|
717
|
-
id: string;
|
|
794
|
+
id: string; // "openai" | "azure" | registry id
|
|
718
795
|
label: string;
|
|
719
|
-
kind: RegistryProviderKind;
|
|
796
|
+
kind: RegistryProviderKind; // "api-key" (built-ins + most registry) | "codex-subscription"
|
|
720
797
|
api: ModelProviderApi;
|
|
721
798
|
builtin: boolean;
|
|
722
799
|
baseUrl?: string | undefined;
|
|
723
800
|
apiKey?: string | undefined;
|
|
724
801
|
defaultQuery?: Record<string, string> | undefined;
|
|
725
802
|
defaultHeaders?: Record<string, string> | undefined;
|
|
726
|
-
compactionMode: ContextCompactionMode; // "server" only for built-in OpenAI; "client" otherwise
|
|
727
803
|
}
|
|
728
804
|
|
|
729
805
|
/** A single exposed model + the provider that serves it. */
|
|
@@ -734,17 +810,31 @@ export interface ConfiguredModel {
|
|
|
734
810
|
providerLabel: string;
|
|
735
811
|
api: ModelProviderApi;
|
|
736
812
|
contextWindowTokens?: number | undefined;
|
|
813
|
+
effectiveContextWindowTokens?: number | undefined;
|
|
814
|
+
autoCompactTokenLimit?: number | undefined;
|
|
737
815
|
reasoningEffort: boolean;
|
|
738
816
|
hostedWebSearch: boolean;
|
|
739
817
|
}
|
|
740
818
|
|
|
741
819
|
export const defaultModelPricing: Record<string, ModelPricing> = {
|
|
742
|
-
"gpt-5.
|
|
820
|
+
"gpt-5.6-sol": {
|
|
743
821
|
inputMicrosPerMillionTokens: 5_000_000,
|
|
744
822
|
cachedInputMicrosPerMillionTokens: 500_000,
|
|
745
823
|
outputMicrosPerMillionTokens: 30_000_000,
|
|
746
824
|
marginBps: 2_500,
|
|
747
825
|
},
|
|
826
|
+
"gpt-5.6-terra": {
|
|
827
|
+
inputMicrosPerMillionTokens: 2_500_000,
|
|
828
|
+
cachedInputMicrosPerMillionTokens: 250_000,
|
|
829
|
+
outputMicrosPerMillionTokens: 15_000_000,
|
|
830
|
+
marginBps: 2_500,
|
|
831
|
+
},
|
|
832
|
+
"gpt-5.6-luna": {
|
|
833
|
+
inputMicrosPerMillionTokens: 1_000_000,
|
|
834
|
+
cachedInputMicrosPerMillionTokens: 100_000,
|
|
835
|
+
outputMicrosPerMillionTokens: 6_000_000,
|
|
836
|
+
marginBps: 2_500,
|
|
837
|
+
},
|
|
748
838
|
"gpt-5.4": {
|
|
749
839
|
inputMicrosPerMillionTokens: 2_500_000,
|
|
750
840
|
cachedInputMicrosPerMillionTokens: 250_000,
|
|
@@ -830,7 +920,10 @@ export type SandboxRequiredEnv = {
|
|
|
830
920
|
env: string;
|
|
831
921
|
};
|
|
832
922
|
|
|
833
|
-
export const SANDBOX_REQUIRED_ENV: Record<
|
|
923
|
+
export const SANDBOX_REQUIRED_ENV: Record<
|
|
924
|
+
z.infer<typeof SandboxBackend>,
|
|
925
|
+
readonly SandboxRequiredEnv[]
|
|
926
|
+
> = {
|
|
834
927
|
// docker/local/none need no credentials (local dev container / in-process / off).
|
|
835
928
|
docker: [],
|
|
836
929
|
local: [],
|
|
@@ -840,21 +933,11 @@ export const SANDBOX_REQUIRED_ENV: Record<z.infer<typeof SandboxBackend>, readon
|
|
|
840
933
|
{ field: "modalTokenId", env: "OPENGENI_MODAL_TOKEN_ID" },
|
|
841
934
|
{ field: "modalTokenSecret", env: "OPENGENI_MODAL_TOKEN_SECRET" },
|
|
842
935
|
],
|
|
843
|
-
daytona: [
|
|
844
|
-
|
|
845
|
-
],
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
],
|
|
849
|
-
e2b: [
|
|
850
|
-
{ field: "e2bApiKey", env: "OPENGENI_E2B_API_KEY" },
|
|
851
|
-
],
|
|
852
|
-
blaxel: [
|
|
853
|
-
{ field: "blaxelApiKey", env: "OPENGENI_BLAXEL_API_KEY" },
|
|
854
|
-
],
|
|
855
|
-
cloudflare: [
|
|
856
|
-
{ field: "cloudflareWorkerUrl", env: "OPENGENI_CLOUDFLARE_WORKER_URL" },
|
|
857
|
-
],
|
|
936
|
+
daytona: [{ field: "daytonaApiKey", env: "OPENGENI_DAYTONA_API_KEY" }],
|
|
937
|
+
runloop: [{ field: "runloopApiKey", env: "OPENGENI_RUNLOOP_API_KEY" }],
|
|
938
|
+
e2b: [{ field: "e2bApiKey", env: "OPENGENI_E2B_API_KEY" }],
|
|
939
|
+
blaxel: [{ field: "blaxelApiKey", env: "OPENGENI_BLAXEL_API_KEY" }],
|
|
940
|
+
cloudflare: [{ field: "cloudflareWorkerUrl", env: "OPENGENI_CLOUDFLARE_WORKER_URL" }],
|
|
858
941
|
vercel: [
|
|
859
942
|
{ field: "vercelToken", env: "OPENGENI_VERCEL_TOKEN" },
|
|
860
943
|
{ field: "vercelProjectId", env: "OPENGENI_VERCEL_PROJECT_ID" },
|
|
@@ -880,7 +963,10 @@ export function getSettings(): Settings {
|
|
|
880
963
|
const raw = {
|
|
881
964
|
serviceName: optional("OPENGENI_SERVICE_NAME"),
|
|
882
965
|
environment: optional("OPENGENI_ENVIRONMENT"),
|
|
883
|
-
deploymentRevision:
|
|
966
|
+
deploymentRevision:
|
|
967
|
+
optional("OPENGENI_DEPLOYMENT_REVISION") ??
|
|
968
|
+
optional("SOURCE_VERSION") ??
|
|
969
|
+
optional("GITHUB_SHA"),
|
|
884
970
|
serverVersion: optional("OPENGENI_SERVER_VERSION"),
|
|
885
971
|
databaseUrl: optional("OPENGENI_DATABASE_URL"),
|
|
886
972
|
dbSchema: optional("OPENGENI_DB_SCHEMA"),
|
|
@@ -890,12 +976,16 @@ export function getSettings(): Settings {
|
|
|
890
976
|
temporalNamespace: optional("OPENGENI_TEMPORAL_NAMESPACE"),
|
|
891
977
|
temporalTaskQueue: optional("OPENGENI_TEMPORAL_TASK_QUEUE"),
|
|
892
978
|
startupDependencyRetryAttempts: optional("OPENGENI_STARTUP_DEPENDENCY_RETRY_ATTEMPTS"),
|
|
893
|
-
startupDependencyRetryInitialDelayMs: optional(
|
|
979
|
+
startupDependencyRetryInitialDelayMs: optional(
|
|
980
|
+
"OPENGENI_STARTUP_DEPENDENCY_RETRY_INITIAL_DELAY_MS",
|
|
981
|
+
),
|
|
894
982
|
startupDependencyRetryMaxDelayMs: optional("OPENGENI_STARTUP_DEPENDENCY_RETRY_MAX_DELAY_MS"),
|
|
895
983
|
observabilityStructuredLogs: optional("OPENGENI_OBSERVABILITY_STRUCTURED_LOGS"),
|
|
896
984
|
observabilityMetricsEnabled: optional("OPENGENI_OBSERVABILITY_METRICS_ENABLED"),
|
|
897
|
-
observabilityOtlpEndpoint:
|
|
898
|
-
|
|
985
|
+
observabilityOtlpEndpoint:
|
|
986
|
+
optional("OPENGENI_OTEL_EXPORTER_OTLP_ENDPOINT") ?? optional("OTEL_EXPORTER_OTLP_ENDPOINT"),
|
|
987
|
+
observabilityOtlpHeaders:
|
|
988
|
+
optional("OPENGENI_OTEL_EXPORTER_OTLP_HEADERS") ?? optional("OTEL_EXPORTER_OTLP_HEADERS"),
|
|
899
989
|
publicBaseUrl: optional("OPENGENI_PUBLIC_BASE_URL"),
|
|
900
990
|
agentReleasesBaseUrl: optional("OPENGENI_AGENT_RELEASES_BASE_URL"),
|
|
901
991
|
productAccessMode: optional("OPENGENI_PRODUCT_ACCESS_MODE"),
|
|
@@ -912,21 +1002,19 @@ export function getSettings(): Settings {
|
|
|
912
1002
|
environmentsEncryptionKey: optional("OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY"),
|
|
913
1003
|
integrationsEnabled: optional("OPENGENI_INTEGRATIONS_ENABLED"),
|
|
914
1004
|
integrationsStateSecret: optional("OPENGENI_INTEGRATIONS_STATE_SECRET"),
|
|
915
|
-
integrationsAllowPrivateNetworkTargets: optional(
|
|
1005
|
+
integrationsAllowPrivateNetworkTargets: optional(
|
|
1006
|
+
"OPENGENI_INTEGRATIONS_ALLOW_PRIVATE_NETWORK_TARGETS",
|
|
1007
|
+
),
|
|
916
1008
|
integrationsOauthClientsJson: optional("OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON"),
|
|
917
1009
|
goalMaxAutoContinuations: optional("OPENGENI_GOAL_MAX_AUTO_CONTINUATIONS"),
|
|
918
1010
|
goalNoProgressLimit: optional("OPENGENI_GOAL_NO_PROGRESS_LIMIT"),
|
|
1011
|
+
childCompletionParentWakeEnabled: optional("OPENGENI_CHILD_COMPLETION_PARENT_WAKE_ENABLED"),
|
|
919
1012
|
agentMaxModelCallsPerTurn: optional("OPENGENI_AGENT_MAX_MODEL_CALLS_PER_TURN"),
|
|
920
|
-
sessionHistorySource: optional("OPENGENI_SESSION_HISTORY_SOURCE"),
|
|
921
|
-
contextCompactionMode: optional("OPENGENI_CONTEXT_COMPACTION_MODE"),
|
|
922
1013
|
contextWindowTokens: optional("OPENGENI_CONTEXT_WINDOW_TOKENS"),
|
|
1014
|
+
contextEffectiveWindowTokens: optional("OPENGENI_CONTEXT_EFFECTIVE_WINDOW_TOKENS"),
|
|
923
1015
|
contextCompactionThresholdRatio: optional("OPENGENI_COMPACTION_THRESHOLD_RATIO"),
|
|
924
1016
|
contextReservedOutputTokens: optional("OPENGENI_CONTEXT_RESERVED_OUTPUT_TOKENS"),
|
|
925
|
-
|
|
926
|
-
contextCompactSoftFraction: optional("OPENGENI_CONTEXT_COMPACT_SOFT_FRACTION"),
|
|
927
|
-
contextCompactHardFraction: optional("OPENGENI_CONTEXT_COMPACT_HARD_FRACTION"),
|
|
928
|
-
contextKeepRecentTokens: optional("OPENGENI_CONTEXT_KEEP_RECENT_TOKENS"),
|
|
929
|
-
contextSummaryMaxTokens: optional("OPENGENI_CONTEXT_SUMMARY_MAX_TOKENS"),
|
|
1017
|
+
contextAutoCompactThresholdTokens: optional("OPENGENI_CONTEXT_AUTO_COMPACT_THRESHOLD_TOKENS"),
|
|
930
1018
|
authRequired: optional("OPENGENI_AUTH_REQUIRED"),
|
|
931
1019
|
accessKey: optional("OPENGENI_ACCESS_KEY"),
|
|
932
1020
|
authAllowHealth: optional("OPENGENI_AUTH_ALLOW_HEALTH"),
|
|
@@ -945,6 +1033,7 @@ export function getSettings(): Settings {
|
|
|
945
1033
|
modelProvidersJson: optional("OPENGENI_MODEL_PROVIDERS_JSON"),
|
|
946
1034
|
codexSubscriptionEnabled: optional("OPENGENI_CODEX_SUBSCRIPTION_ENABLED"),
|
|
947
1035
|
codexToolSearchEnabled: optional("OPENGENI_CODEX_TOOL_SEARCH_ENABLED"),
|
|
1036
|
+
codexCredentialLeasingEnabled: optional("OPENGENI_CODEX_CREDENTIAL_LEASING_ENABLED"),
|
|
948
1037
|
codexProductSku: optional("OPENGENI_CODEX_PRODUCT_SKU"),
|
|
949
1038
|
codexRotationNearExhaustionPct: optional("OPENGENI_CODEX_ROTATION_NEAR_EXHAUSTION_PCT"),
|
|
950
1039
|
openaiReasoningEffort: optional("OPENGENI_OPENAI_REASONING_EFFORT"),
|
|
@@ -968,6 +1057,7 @@ export function getSettings(): Settings {
|
|
|
968
1057
|
dockerNetwork: optional("OPENGENI_DOCKER_NETWORK"),
|
|
969
1058
|
modalAppName: optional("OPENGENI_MODAL_APP_NAME"),
|
|
970
1059
|
modalImageRef: optional("OPENGENI_MODAL_IMAGE_REF"),
|
|
1060
|
+
modalImageRegistrySecret: optional("OPENGENI_MODAL_IMAGE_REGISTRY_SECRET"),
|
|
971
1061
|
modalTimeoutSeconds: optional("OPENGENI_MODAL_TIMEOUT_SECONDS"),
|
|
972
1062
|
modalTokenId: optional("OPENGENI_MODAL_TOKEN_ID"),
|
|
973
1063
|
modalTokenSecret: optional("OPENGENI_MODAL_TOKEN_SECRET"),
|
|
@@ -983,6 +1073,7 @@ export function getSettings(): Settings {
|
|
|
983
1073
|
computerUseEnabled: optional("OPENGENI_COMPUTER_USE_ENABLED"),
|
|
984
1074
|
computerUseReadOnly: optional("OPENGENI_COMPUTER_USE_READONLY"),
|
|
985
1075
|
recordingEnabled: optional("OPENGENI_RECORDING_ENABLED"),
|
|
1076
|
+
workspaceCaptureEnabled: optional("OPENGENI_WORKSPACE_CAPTURE"),
|
|
986
1077
|
recordingDefaultCodec: optional("OPENGENI_RECORDING_DEFAULT_CODEC"),
|
|
987
1078
|
recordingFramerate: optional("OPENGENI_RECORDING_FRAMERATE"),
|
|
988
1079
|
recordingMaxSeconds: optional("OPENGENI_RECORDING_MAX_SECONDS"),
|
|
@@ -1022,7 +1113,9 @@ export function getSettings(): Settings {
|
|
|
1022
1113
|
vercelTeamId: optional("OPENGENI_VERCEL_TEAM_ID"),
|
|
1023
1114
|
vercelRuntime: optional("OPENGENI_VERCEL_RUNTIME"),
|
|
1024
1115
|
sandboxOwnershipEnabled: optional("OPENGENI_SANDBOX_OWNERSHIP_ENABLED"),
|
|
1116
|
+
sandboxLazyProvisionEnabled: optional("OPENGENI_SANDBOX_LAZY_PROVISION"),
|
|
1025
1117
|
sandboxSelfhostedEnabled: optional("OPENGENI_SANDBOX_SELFHOSTED_ENABLED"),
|
|
1118
|
+
agentOpStreamEnabled: optional("OPENGENI_AGENT_OP_STREAM_ENABLED"),
|
|
1026
1119
|
enrollmentSigningSecret: optional("OPENGENI_ENROLLMENT_SIGNING_SECRET"),
|
|
1027
1120
|
selfhostedNatsUrl: optional("OPENGENI_SELFHOSTED_NATS_URL"),
|
|
1028
1121
|
selfhostedRelayUrl: optional("OPENGENI_SELFHOSTED_RELAY_URL"),
|
|
@@ -1034,13 +1127,20 @@ export function getSettings(): Settings {
|
|
|
1034
1127
|
selfhostedNatsCalloutPassword: optional("OPENGENI_SELFHOSTED_NATS_CALLOUT_PASSWORD"),
|
|
1035
1128
|
selfhostedNatsControlUser: optional("OPENGENI_SELFHOSTED_NATS_CONTROL_USER"),
|
|
1036
1129
|
selfhostedNatsControlPassword: optional("OPENGENI_SELFHOSTED_NATS_CONTROL_PASSWORD"),
|
|
1130
|
+
sandboxSelfhostedExecTimeoutMs: optional("OPENGENI_SANDBOX_SELFHOSTED_EXEC_TIMEOUT_MS"),
|
|
1131
|
+
sandboxSelfhostedControlTimeoutMs: optional("OPENGENI_SANDBOX_SELFHOSTED_CONTROL_TIMEOUT_MS"),
|
|
1037
1132
|
sandboxLeaseReaperPeriodMs: optional("OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS"),
|
|
1038
1133
|
sandboxViewerHolderTtlMs: optional("OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS"),
|
|
1039
1134
|
sandboxIdleGraceMs: optional("OPENGENI_SANDBOX_IDLE_GRACE_MS"),
|
|
1135
|
+
sandboxSnapshotIntervalMs: optional("OPENGENI_SANDBOX_SNAPSHOT_INTERVAL_MS"),
|
|
1136
|
+
sandboxSnapshotTimeoutMs: optional("OPENGENI_SANDBOX_SNAPSHOT_TIMEOUT_MS"),
|
|
1040
1137
|
sandboxLeaseTtlMs: optional("OPENGENI_SANDBOX_LEASE_TTL_MS"),
|
|
1041
1138
|
sandboxLeaseWarmingTtlMs: optional("OPENGENI_SANDBOX_LEASE_WARMING_TTL_MS"),
|
|
1042
1139
|
sandboxWarmingTimeoutMs: optional("OPENGENI_SANDBOX_WARMING_TIMEOUT_MS"),
|
|
1043
|
-
|
|
1140
|
+
rigSetupTimeoutMs: optional("OPENGENI_RIG_SETUP_TIMEOUT_MS"),
|
|
1141
|
+
sandboxWarmRateMicrosPerSecondJson: optional(
|
|
1142
|
+
"OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON",
|
|
1143
|
+
),
|
|
1044
1144
|
sandboxMaxWarmSecondsPerWorkspace: optional("OPENGENI_SANDBOX_MAX_WARM_SECONDS_PER_WORKSPACE"),
|
|
1045
1145
|
sandboxPreparationProfiles: optional("OPENGENI_SANDBOX_PREPARATION_PROFILES"),
|
|
1046
1146
|
sandboxEnvAllowlist: optional("OPENGENI_SANDBOX_ENV_ALLOWLIST"),
|
|
@@ -1117,7 +1217,10 @@ export function effectiveModalIdleTimeoutSeconds(settings: Settings): number {
|
|
|
1117
1217
|
return settings.modalIdleTimeoutSeconds ?? settings.modalTimeoutSeconds;
|
|
1118
1218
|
}
|
|
1119
1219
|
|
|
1120
|
-
export function collectSandboxEnvironment(
|
|
1220
|
+
export function collectSandboxEnvironment(
|
|
1221
|
+
settings: Settings,
|
|
1222
|
+
source: NodeJS.ProcessEnv = process.env,
|
|
1223
|
+
): Record<string, string> {
|
|
1121
1224
|
const out: Record<string, string> = {};
|
|
1122
1225
|
for (const name of sandboxEnvironmentVariableNames(settings)) {
|
|
1123
1226
|
const value = source[name];
|
|
@@ -1149,8 +1252,14 @@ export function resolveProviderApiKey(
|
|
|
1149
1252
|
return undefined;
|
|
1150
1253
|
}
|
|
1151
1254
|
|
|
1152
|
-
/**
|
|
1153
|
-
|
|
1255
|
+
/**
|
|
1256
|
+
* The built-in provider's stable id: "openai" on the OpenAI platform, "azure"
|
|
1257
|
+
* on Azure. Exported because the workspace model-policy gate must attribute
|
|
1258
|
+
* the legacy resolveTurnModel-null fallback (which routes to this built-in
|
|
1259
|
+
* client) to the SAME identity the router uses — otherwise a policy blocking
|
|
1260
|
+
* the built-in could be bypassed through the null-resolution path.
|
|
1261
|
+
*/
|
|
1262
|
+
export function builtinProviderId(settings: Pick<Settings, "openaiProvider">): string {
|
|
1154
1263
|
return settings.openaiProvider === "azure" ? "azure" : "openai";
|
|
1155
1264
|
}
|
|
1156
1265
|
|
|
@@ -1160,9 +1269,8 @@ function builtinProviderLabel(settings: Pick<Settings, "openaiProvider">): strin
|
|
|
1160
1269
|
|
|
1161
1270
|
/**
|
|
1162
1271
|
* Every provider a client may route to: the built-in OpenAI/Azure provider
|
|
1163
|
-
* first (id "openai"/"azure", always "responses",
|
|
1164
|
-
*
|
|
1165
|
-
* order (compactionMode "client"). Client-construction inputs are filled from
|
|
1272
|
+
* first (id "openai"/"azure", always "responses"), then each registry provider
|
|
1273
|
+
* in declaration order. Client-construction inputs are filled from
|
|
1166
1274
|
* the existing flat openai/azure settings for the built-in, and from the
|
|
1167
1275
|
* registry entry for the rest. Registry ids may not collide with the built-in
|
|
1168
1276
|
* id — validateSettings rejects that at boot.
|
|
@@ -1174,7 +1282,6 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
|
|
|
1174
1282
|
kind: "api-key",
|
|
1175
1283
|
api: "responses",
|
|
1176
1284
|
builtin: true,
|
|
1177
|
-
compactionMode: resolveContextCompactionMode(settings),
|
|
1178
1285
|
};
|
|
1179
1286
|
if (settings.openaiProvider === "azure") {
|
|
1180
1287
|
builtin.baseUrl = settings.azureOpenaiBaseUrl ?? settings.azureOpenaiEndpoint;
|
|
@@ -1183,21 +1290,43 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
|
|
|
1183
1290
|
builtin.baseUrl = settings.openaiBaseUrl;
|
|
1184
1291
|
builtin.apiKey = settings.openaiApiKey;
|
|
1185
1292
|
}
|
|
1186
|
-
const registry = parseModelProvidersJson(settings.modelProvidersJson).map(
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1293
|
+
const registry = parseModelProvidersJson(settings.modelProvidersJson).map(
|
|
1294
|
+
(provider): ResolvedModelProvider => ({
|
|
1295
|
+
id: provider.id,
|
|
1296
|
+
label: provider.label ?? provider.id,
|
|
1297
|
+
kind: provider.kind,
|
|
1298
|
+
api: provider.api,
|
|
1299
|
+
builtin: false,
|
|
1300
|
+
baseUrl: provider.baseUrl,
|
|
1301
|
+
apiKey: resolveProviderApiKey(provider),
|
|
1302
|
+
defaultQuery: provider.defaultQuery,
|
|
1303
|
+
defaultHeaders: provider.defaultHeaders,
|
|
1304
|
+
}),
|
|
1305
|
+
);
|
|
1198
1306
|
return [builtin, ...registry];
|
|
1199
1307
|
}
|
|
1200
1308
|
|
|
1309
|
+
/**
|
|
1310
|
+
* The provider identity a model id resolves to, for workspace model-policy
|
|
1311
|
+
* evaluation — MUST agree with the real router (resolveTurnModel /
|
|
1312
|
+
* MultiProviderModelProvider) on every case:
|
|
1313
|
+
* - `codex/<slug>` → the codex-subscription provider id, ALWAYS. With no
|
|
1314
|
+
* active subscription the router fails loud (CodexSubscriptionUnavailableError),
|
|
1315
|
+
* never the built-in — so attributing by prefix is exact even against BASE
|
|
1316
|
+
* settings where the overlay provider is not injected.
|
|
1317
|
+
* - a configured model id → its configuredModels providerId (registry or built-in).
|
|
1318
|
+
* - anything else → the built-in id: an unknown id is the legacy
|
|
1319
|
+
* resolveTurnModel-null fallback, which the built-in OpenAI/Azure client
|
|
1320
|
+
* serves. A policy blocking the built-in must block this path too.
|
|
1321
|
+
*/
|
|
1322
|
+
export function policyProviderIdForModel(settings: Settings, modelId: string): string {
|
|
1323
|
+
if (modelId.startsWith(CODEX_MODEL_ID_PREFIX)) {
|
|
1324
|
+
return CODEX_PROVIDER_ID;
|
|
1325
|
+
}
|
|
1326
|
+
const configured = configuredModels(settings).find((model) => model.id === modelId);
|
|
1327
|
+
return configured?.providerId ?? builtinProviderId(settings);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1201
1330
|
/**
|
|
1202
1331
|
* Every model a client may use, the built-in provider's models first
|
|
1203
1332
|
* (configuredAllowedModels-from-openai, mapped to "responses" with
|
|
@@ -1220,17 +1349,22 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
|
|
|
1220
1349
|
// contains "/") that a registry actually owns is never a valid Azure/OpenAI
|
|
1221
1350
|
// deployment name, and a `codex/`-prefixed id never is either — exclude both
|
|
1222
1351
|
// from the built-in list. A BARE id a registry merely redeclares (e.g.
|
|
1223
|
-
// "gpt-5.
|
|
1352
|
+
// "gpt-5.6-sol") is left in place so the built-in still wins it via the first-wins
|
|
1224
1353
|
// de-dup below (preserving the documented built-in-precedence contract). When
|
|
1225
1354
|
// a codex/ id has NO codex provider injected (no active subscription) it then
|
|
1226
1355
|
// resolves to nothing and getModel fails loud with
|
|
1227
1356
|
// CodexSubscriptionUnavailableError instead of mis-routing to Azure.
|
|
1228
1357
|
const registryOwnedIds = new Set(
|
|
1229
|
-
parseModelProvidersJson(settings.modelProvidersJson).flatMap((provider) =>
|
|
1358
|
+
parseModelProvidersJson(settings.modelProvidersJson).flatMap((provider) =>
|
|
1359
|
+
provider.models.map((model) => model.id),
|
|
1360
|
+
),
|
|
1230
1361
|
);
|
|
1231
1362
|
const isRegistryNamespaced = (id: string): boolean =>
|
|
1232
1363
|
id.startsWith(CODEX_MODEL_ID_PREFIX) || (id.includes("/") && registryOwnedIds.has(id));
|
|
1233
|
-
const out: ConfiguredModel[] = uniqueValues([
|
|
1364
|
+
const out: ConfiguredModel[] = uniqueValues([
|
|
1365
|
+
settings.openaiModel,
|
|
1366
|
+
...splitCsv(settings.openaiAllowedModels),
|
|
1367
|
+
])
|
|
1234
1368
|
.filter((id) => !isRegistryNamespaced(id))
|
|
1235
1369
|
.map((id) => ({
|
|
1236
1370
|
id,
|
|
@@ -1251,7 +1385,15 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
|
|
|
1251
1385
|
providerId: provider.id,
|
|
1252
1386
|
providerLabel,
|
|
1253
1387
|
api: provider.api,
|
|
1254
|
-
...(model.contextWindowTokens === undefined
|
|
1388
|
+
...(model.contextWindowTokens === undefined
|
|
1389
|
+
? {}
|
|
1390
|
+
: { contextWindowTokens: model.contextWindowTokens }),
|
|
1391
|
+
...(model.effectiveContextWindowTokens === undefined
|
|
1392
|
+
? {}
|
|
1393
|
+
: { effectiveContextWindowTokens: model.effectiveContextWindowTokens }),
|
|
1394
|
+
...(model.autoCompactTokenLimit === undefined
|
|
1395
|
+
? {}
|
|
1396
|
+
: { autoCompactTokenLimit: model.autoCompactTokenLimit }),
|
|
1255
1397
|
reasoningEffort: model.reasoningEffort ?? false,
|
|
1256
1398
|
hostedWebSearch: model.hostedWebSearch ?? false,
|
|
1257
1399
|
});
|
|
@@ -1292,7 +1434,9 @@ export function resolveModelProvider(
|
|
|
1292
1434
|
if (!model) {
|
|
1293
1435
|
return undefined;
|
|
1294
1436
|
}
|
|
1295
|
-
const provider = configuredProviders(settings).find(
|
|
1437
|
+
const provider = configuredProviders(settings).find(
|
|
1438
|
+
(candidate) => candidate.id === model.providerId,
|
|
1439
|
+
);
|
|
1296
1440
|
if (!provider) {
|
|
1297
1441
|
return undefined;
|
|
1298
1442
|
}
|
|
@@ -1322,49 +1466,49 @@ export function configuredModelPricing(settings: Settings): Record<string, Model
|
|
|
1322
1466
|
}
|
|
1323
1467
|
|
|
1324
1468
|
/**
|
|
1325
|
-
*
|
|
1326
|
-
*
|
|
1327
|
-
* SDK emits context_management; we pass the correct gpt-5.5 threshold).
|
|
1328
|
-
* - "client": run OpenGeni's own client-side compaction (Azure and any other
|
|
1329
|
-
* backend that rejects/ignores context_management).
|
|
1330
|
-
* - "off": neither (legacy unbounded growth; escape hatch).
|
|
1331
|
-
*
|
|
1332
|
-
* "auto" maps to "server" on the OpenAI platform provider and "client"
|
|
1333
|
-
* otherwise — Azure's Responses API returns 400 unsupported_parameter for
|
|
1334
|
-
* context_management, so it must never take the server path.
|
|
1469
|
+
* Usable input-token budget: an explicit model-catalog effective window when
|
|
1470
|
+
* available, otherwise the deployment window minus its output reserve.
|
|
1335
1471
|
*/
|
|
1336
|
-
export
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
case "off":
|
|
1345
|
-
return "off";
|
|
1346
|
-
case "auto":
|
|
1347
|
-
default:
|
|
1348
|
-
return settings.openaiProvider === "openai" ? "server" : "client";
|
|
1472
|
+
export function contextInputBudgetTokens(
|
|
1473
|
+
settings: Pick<
|
|
1474
|
+
Settings,
|
|
1475
|
+
"contextWindowTokens" | "contextEffectiveWindowTokens" | "contextReservedOutputTokens"
|
|
1476
|
+
>,
|
|
1477
|
+
): number {
|
|
1478
|
+
if (settings.contextEffectiveWindowTokens !== undefined) {
|
|
1479
|
+
return Math.min(settings.contextWindowTokens, settings.contextEffectiveWindowTokens);
|
|
1349
1480
|
}
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
/** Usable input-token budget B = window - reserved output. */
|
|
1353
|
-
export function contextInputBudgetTokens(settings: Pick<Settings, "contextWindowTokens" | "contextReservedOutputTokens">): number {
|
|
1354
1481
|
return Math.max(0, settings.contextWindowTokens - settings.contextReservedOutputTokens);
|
|
1355
1482
|
}
|
|
1356
1483
|
|
|
1357
1484
|
/**
|
|
1358
|
-
*
|
|
1359
|
-
*
|
|
1360
|
-
*
|
|
1361
|
-
* fallback for gpt-5.5 (which is absent from its hardcoded window map).
|
|
1485
|
+
* Apply the resolved provider/model's context policy to one turn. Registry
|
|
1486
|
+
* metadata is authoritative when present; deployment defaults remain the
|
|
1487
|
+
* fallback for models that do not declare their own limits.
|
|
1362
1488
|
*/
|
|
1363
|
-
export function
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1489
|
+
export function settingsWithResolvedModelContext(
|
|
1490
|
+
settings: Settings,
|
|
1491
|
+
model: Pick<
|
|
1492
|
+
ConfiguredModel,
|
|
1493
|
+
"contextWindowTokens" | "effectiveContextWindowTokens" | "autoCompactTokenLimit"
|
|
1494
|
+
>,
|
|
1495
|
+
): Settings {
|
|
1496
|
+
const contextWindowTokens = model.contextWindowTokens ?? settings.contextWindowTokens;
|
|
1497
|
+
return {
|
|
1498
|
+
...settings,
|
|
1499
|
+
contextWindowTokens,
|
|
1500
|
+
...(model.effectiveContextWindowTokens === undefined
|
|
1501
|
+
? {}
|
|
1502
|
+
: {
|
|
1503
|
+
contextEffectiveWindowTokens: Math.min(
|
|
1504
|
+
contextWindowTokens,
|
|
1505
|
+
model.effectiveContextWindowTokens,
|
|
1506
|
+
),
|
|
1507
|
+
}),
|
|
1508
|
+
...(model.autoCompactTokenLimit === undefined
|
|
1509
|
+
? {}
|
|
1510
|
+
: { contextAutoCompactThresholdTokens: model.autoCompactTokenLimit }),
|
|
1511
|
+
};
|
|
1368
1512
|
}
|
|
1369
1513
|
|
|
1370
1514
|
export function configuredStaticUsageLimits(settings: Settings): StaticUsageLimitsConfig {
|
|
@@ -1389,20 +1533,31 @@ export function configuredEntitlements(settings: Settings): EntitlementsConfig {
|
|
|
1389
1533
|
};
|
|
1390
1534
|
}
|
|
1391
1535
|
|
|
1392
|
-
export function calculateModelUsageCostMicros(
|
|
1536
|
+
export function calculateModelUsageCostMicros(
|
|
1537
|
+
settings: Settings,
|
|
1538
|
+
model: string,
|
|
1539
|
+
usage: ModelUsageInput,
|
|
1540
|
+
): number {
|
|
1393
1541
|
const pricing = configuredModelPricing(settings)[model];
|
|
1394
1542
|
if (!pricing) {
|
|
1395
1543
|
throw new Error(`Missing model pricing for ${model}`);
|
|
1396
1544
|
}
|
|
1397
|
-
const entries =
|
|
1545
|
+
const entries =
|
|
1546
|
+
usage.requestUsageEntries && usage.requestUsageEntries.length > 0
|
|
1547
|
+
? usage.requestUsageEntries
|
|
1548
|
+
: [usage];
|
|
1398
1549
|
const rawCost = entries.reduce((sum, entry) => sum + calculateEntryCostMicros(pricing, entry), 0);
|
|
1399
1550
|
const marginBps = pricing.marginBps ?? 0;
|
|
1400
|
-
return Math.ceil(rawCost * (10_000 + marginBps) / 10_000);
|
|
1551
|
+
return Math.ceil((rawCost * (10_000 + marginBps)) / 10_000);
|
|
1401
1552
|
}
|
|
1402
1553
|
|
|
1403
|
-
export function configuredAllowedReasoningEfforts(
|
|
1404
|
-
|
|
1405
|
-
|
|
1554
|
+
export function configuredAllowedReasoningEfforts(
|
|
1555
|
+
settings: Settings,
|
|
1556
|
+
): Array<z.infer<typeof ReasoningEffort>> {
|
|
1557
|
+
return uniqueValues([
|
|
1558
|
+
settings.openaiReasoningEffort,
|
|
1559
|
+
...splitCsv(settings.openaiAllowedReasoningEfforts),
|
|
1560
|
+
]).map((value) => ReasoningEffort.parse(value));
|
|
1406
1561
|
}
|
|
1407
1562
|
|
|
1408
1563
|
/**
|
|
@@ -1416,7 +1571,9 @@ export function environmentsEncryptionKeyBytes(settings: Settings): Uint8Array |
|
|
|
1416
1571
|
}
|
|
1417
1572
|
const decoded = Buffer.from(settings.environmentsEncryptionKey, "base64");
|
|
1418
1573
|
if (decoded.length !== 32) {
|
|
1419
|
-
throw new Error(
|
|
1574
|
+
throw new Error(
|
|
1575
|
+
"OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY must be base64 for exactly 32 bytes (generate with: openssl rand -base64 32)",
|
|
1576
|
+
);
|
|
1420
1577
|
}
|
|
1421
1578
|
return new Uint8Array(decoded);
|
|
1422
1579
|
}
|
|
@@ -1443,12 +1600,24 @@ export function dbSearchPath(settings: Pick<Settings, "dbSchema">): string | und
|
|
|
1443
1600
|
}
|
|
1444
1601
|
|
|
1445
1602
|
export function collectGitIdentityEnvironment(settings: Settings): Record<string, string> {
|
|
1446
|
-
return Object.fromEntries(
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1603
|
+
return Object.fromEntries(
|
|
1604
|
+
Object.entries({
|
|
1605
|
+
GIT_AUTHOR_NAME: settings.gitAuthorName,
|
|
1606
|
+
GIT_AUTHOR_EMAIL: settings.gitAuthorEmail,
|
|
1607
|
+
GIT_COMMITTER_NAME: settings.gitCommitterName ?? settings.gitAuthorName,
|
|
1608
|
+
GIT_COMMITTER_EMAIL: settings.gitCommitterEmail ?? settings.gitAuthorEmail,
|
|
1609
|
+
}).filter(
|
|
1610
|
+
(entry): entry is [string, string] =>
|
|
1611
|
+
typeof entry[1] === "string" && entry[1].trim().length > 0,
|
|
1612
|
+
),
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
const DEFAULT_SANDBOX_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
1617
|
+
|
|
1618
|
+
function prependPathEntry(pathValue: string | undefined, entry: string): string {
|
|
1619
|
+
const parts = (pathValue ?? DEFAULT_SANDBOX_PATH).split(":").filter(Boolean);
|
|
1620
|
+
return [entry, ...parts.filter((part) => part !== entry)].join(":");
|
|
1452
1621
|
}
|
|
1453
1622
|
|
|
1454
1623
|
/**
|
|
@@ -1466,20 +1635,22 @@ export function collectGitIdentityEnvironment(settings: Settings): Record<string
|
|
|
1466
1635
|
* workspace environment < the backend-aware HOME default. Reserved-name validation
|
|
1467
1636
|
* at write time keeps workspace values from colliding with platform entries.
|
|
1468
1637
|
*
|
|
1469
|
-
* DELIBERATELY EXCLUDES the per-run, ROTATING
|
|
1470
|
-
*
|
|
1638
|
+
* DELIBERATELY EXCLUDES the per-run, ROTATING git provider token VALUES that
|
|
1639
|
+
* `sandboxEnvironmentForRun` mints when a repository resource is
|
|
1471
1640
|
* attached: that token is minted FRESH per call, so it is not a stable, attach-
|
|
1472
1641
|
* reproducible value and must not be part of the shared base. Under the token-
|
|
1473
|
-
* broker (B1)
|
|
1474
|
-
*
|
|
1475
|
-
*
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
1478
|
-
*
|
|
1479
|
-
*
|
|
1480
|
-
*
|
|
1481
|
-
*
|
|
1482
|
-
*
|
|
1642
|
+
* broker (B1) token VALUES never ride the manifest at all — they are seeded to
|
|
1643
|
+
* FILES inside the box and git/provider CLI auth reads those files. What IS stable
|
|
1644
|
+
* and lives here for provisioned boxes are the token directory / GitHub alias
|
|
1645
|
+
* FILE PATH and wrapper PATH entries: constants derived from HOME, so they
|
|
1646
|
+
* appear IDENTICALLY on BOTH the turn AND every attach manifest (the SDK's
|
|
1647
|
+
* per-turn provided-session env delta stays empty even as tokens rotate). These
|
|
1648
|
+
* helper pointers are deliberately not added for selfhosted/local/none because
|
|
1649
|
+
* the platform never mints or seeds git provider tokens there. The attach
|
|
1650
|
+
* surfaces have only the `Session` (no repo resources) and so never seed a token,
|
|
1651
|
+
* but unwritten files simply yield no auth; the BLOCKING attach-vs-turn error
|
|
1652
|
+
* this helper fixes is for the common (no-repo) and workspace-environment-attached
|
|
1653
|
+
* provisioned-box cases.
|
|
1483
1654
|
*/
|
|
1484
1655
|
export function stableSandboxEnvironmentForRun(
|
|
1485
1656
|
settings: Settings,
|
|
@@ -1498,18 +1669,30 @@ export function stableSandboxEnvironmentForRun(
|
|
|
1498
1669
|
if (settings.sandboxBackend !== "none" && settings.sandboxBackend !== "local") {
|
|
1499
1670
|
environment.HOME ??= descriptor.workspaceRoot;
|
|
1500
1671
|
}
|
|
1501
|
-
// TOKEN-BROKER (B1): the STABLE
|
|
1502
|
-
//
|
|
1503
|
-
//
|
|
1504
|
-
//
|
|
1505
|
-
//
|
|
1506
|
-
//
|
|
1507
|
-
|
|
1508
|
-
|
|
1672
|
+
// TOKEN-BROKER (B1): the STABLE credential FILE PATHS and CLI wrapper PATH for
|
|
1673
|
+
// provisioned boxes only. Constants derived from the resolved HOME (falling
|
|
1674
|
+
// back to the descriptor workspaceRoot), so they are parity-safe — they join
|
|
1675
|
+
// the shared base and therefore appear IDENTICALLY on BOTH the worker-turn
|
|
1676
|
+
// manifest AND every API-direct attach manifest. Only PATHS are stable; token
|
|
1677
|
+
// VALUES live exclusively in files that runtime seeds off-manifest.
|
|
1678
|
+
const provisionedGitHelperBackend =
|
|
1679
|
+
settings.sandboxBackend !== "none" &&
|
|
1680
|
+
settings.sandboxBackend !== "local" &&
|
|
1681
|
+
settings.sandboxBackend !== "selfhosted";
|
|
1682
|
+
if (provisionedGitHelperBackend) {
|
|
1683
|
+
const home = environment.HOME ?? descriptor.workspaceRoot;
|
|
1684
|
+
environment.OPENGENI_GIT_CREDENTIALS_DIR ??= `${home}/.opengeni/git-credentials`;
|
|
1685
|
+
environment.OPENGENI_GIT_TOKEN_FILE ??= `${home}/.opengeni/git-token`;
|
|
1686
|
+
environment.OPENGENI_GIT_CLI_WRAPPER_DIR ??= `${home}/.opengeni/bin`;
|
|
1687
|
+
environment.PATH = prependPathEntry(environment.PATH, environment.OPENGENI_GIT_CLI_WRAPPER_DIR);
|
|
1688
|
+
}
|
|
1509
1689
|
if (settings.toolspaceEnabled) {
|
|
1510
1690
|
environment.OPENGENI_TOOLSPACE_TOKEN_FILE ??= `${environment.HOME ?? descriptor.workspaceRoot}/.opengeni/toolspace-token`;
|
|
1511
1691
|
if (options.workspaceId) {
|
|
1512
|
-
environment.OPENGENI_TOOLSPACE_URL ??= firstPartyMcpWorkspaceUrl(
|
|
1692
|
+
environment.OPENGENI_TOOLSPACE_URL ??= firstPartyMcpWorkspaceUrl(
|
|
1693
|
+
settings,
|
|
1694
|
+
options.workspaceId,
|
|
1695
|
+
);
|
|
1513
1696
|
}
|
|
1514
1697
|
}
|
|
1515
1698
|
return environment;
|
|
@@ -1522,11 +1705,45 @@ export function stableSandboxEnvironmentForRun(
|
|
|
1522
1705
|
* an attach-warmed cold box carries the IDENTICAL manifest env a later repo turn
|
|
1523
1706
|
* declares (env parity — see applyGitAuthPointerEnvironment).
|
|
1524
1707
|
*/
|
|
1525
|
-
export function hasGitHubRepositorySelection(
|
|
1708
|
+
export function hasGitHubRepositorySelection(
|
|
1709
|
+
resources: ReadonlyArray<{
|
|
1710
|
+
kind: string;
|
|
1711
|
+
provider?: unknown;
|
|
1712
|
+
installationId?: unknown;
|
|
1713
|
+
repositoryId?: unknown;
|
|
1714
|
+
githubInstallationId?: unknown;
|
|
1715
|
+
githubRepositoryId?: unknown;
|
|
1716
|
+
}>,
|
|
1717
|
+
): boolean {
|
|
1526
1718
|
const positive = (value: unknown): boolean =>
|
|
1527
|
-
(typeof value === "number" && Number.isInteger(value) && value > 0)
|
|
1528
|
-
|
|
1529
|
-
return resources.some(
|
|
1719
|
+
(typeof value === "number" && Number.isInteger(value) && value > 0) ||
|
|
1720
|
+
(typeof value === "string" && /^\d+$/.test(value) && Number(value) > 0);
|
|
1721
|
+
return resources.some(
|
|
1722
|
+
(resource) =>
|
|
1723
|
+
resource.kind === "repository" &&
|
|
1724
|
+
((positive(resource.githubInstallationId) && positive(resource.githubRepositoryId)) ||
|
|
1725
|
+
(resource.provider === "github" &&
|
|
1726
|
+
positive(resource.installationId) &&
|
|
1727
|
+
positive(resource.repositoryId))),
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
export function hasGitCredentialRepositorySelection(
|
|
1732
|
+
resources: ReadonlyArray<{
|
|
1733
|
+
kind: string;
|
|
1734
|
+
provider?: unknown;
|
|
1735
|
+
githubInstallationId?: unknown;
|
|
1736
|
+
githubRepositoryId?: unknown;
|
|
1737
|
+
}>,
|
|
1738
|
+
): boolean {
|
|
1739
|
+
return resources.some(
|
|
1740
|
+
(resource) =>
|
|
1741
|
+
resource.kind === "repository" &&
|
|
1742
|
+
(resource.provider === "github" ||
|
|
1743
|
+
resource.provider === "gitlab" ||
|
|
1744
|
+
resource.provider === "azure_devops" ||
|
|
1745
|
+
hasGitHubRepositorySelection([resource])),
|
|
1746
|
+
);
|
|
1530
1747
|
}
|
|
1531
1748
|
|
|
1532
1749
|
/**
|
|
@@ -1574,7 +1791,9 @@ export type StartupRetryOptions = {
|
|
|
1574
1791
|
}) => void;
|
|
1575
1792
|
};
|
|
1576
1793
|
|
|
1577
|
-
export function startupRetryOptions(
|
|
1794
|
+
export function startupRetryOptions(
|
|
1795
|
+
settings: Settings,
|
|
1796
|
+
): Required<Omit<StartupRetryOptions, "onRetry">> {
|
|
1578
1797
|
return {
|
|
1579
1798
|
attempts: settings.startupDependencyRetryAttempts,
|
|
1580
1799
|
initialDelayMs: settings.startupDependencyRetryInitialDelayMs,
|
|
@@ -1624,10 +1843,14 @@ export function sandboxLifecycleHookIds(settings: Settings): string[] {
|
|
|
1624
1843
|
}
|
|
1625
1844
|
|
|
1626
1845
|
function sandboxPreparationProfileNames(settings: Settings): string[] {
|
|
1627
|
-
const profiles = splitCsv(settings.sandboxPreparationProfiles).map((value) =>
|
|
1846
|
+
const profiles = splitCsv(settings.sandboxPreparationProfiles).map((value) =>
|
|
1847
|
+
value.toLowerCase(),
|
|
1848
|
+
);
|
|
1628
1849
|
if (profiles.includes("none")) {
|
|
1629
1850
|
if (profiles.length > 1) {
|
|
1630
|
-
throw new Error(
|
|
1851
|
+
throw new Error(
|
|
1852
|
+
"OPENGENI_SANDBOX_PREPARATION_PROFILES cannot combine none with other profiles",
|
|
1853
|
+
);
|
|
1631
1854
|
}
|
|
1632
1855
|
return ["none"];
|
|
1633
1856
|
}
|
|
@@ -1661,7 +1884,7 @@ export function parseMcpServers(raw: string | undefined): unknown[] | undefined
|
|
|
1661
1884
|
return parsed;
|
|
1662
1885
|
} catch (error) {
|
|
1663
1886
|
const message = error instanceof Error ? error.message : String(error);
|
|
1664
|
-
throw new Error(`OPENGENI_MCP_SERVERS must be a JSON array: ${message}
|
|
1887
|
+
throw new Error(`OPENGENI_MCP_SERVERS must be a JSON array: ${message}`, { cause: error });
|
|
1665
1888
|
}
|
|
1666
1889
|
}
|
|
1667
1890
|
|
|
@@ -1674,7 +1897,7 @@ export function parseModelPricingJson(raw: string): Record<string, ModelPricing>
|
|
|
1674
1897
|
parsed = JSON.parse(raw);
|
|
1675
1898
|
} catch (error) {
|
|
1676
1899
|
const message = error instanceof Error ? error.message : String(error);
|
|
1677
|
-
throw new Error(`OPENGENI_MODEL_PRICING_JSON must be valid JSON: ${message}
|
|
1900
|
+
throw new Error(`OPENGENI_MODEL_PRICING_JSON must be valid JSON: ${message}`, { cause: error });
|
|
1678
1901
|
}
|
|
1679
1902
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1680
1903
|
throw new Error("OPENGENI_MODEL_PRICING_JSON must be a JSON object keyed by model name");
|
|
@@ -1702,19 +1925,28 @@ export function parseSandboxWarmRateJson(raw: string): Record<string, number> {
|
|
|
1702
1925
|
parsed = JSON.parse(raw);
|
|
1703
1926
|
} catch (error) {
|
|
1704
1927
|
const message = error instanceof Error ? error.message : String(error);
|
|
1705
|
-
throw new Error(
|
|
1928
|
+
throw new Error(
|
|
1929
|
+
`OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON must be valid JSON: ${message}`,
|
|
1930
|
+
{ cause: error },
|
|
1931
|
+
);
|
|
1706
1932
|
}
|
|
1707
1933
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1708
|
-
throw new Error(
|
|
1934
|
+
throw new Error(
|
|
1935
|
+
"OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON must be a JSON object keyed by backend name",
|
|
1936
|
+
);
|
|
1709
1937
|
}
|
|
1710
1938
|
const out: Record<string, number> = {};
|
|
1711
1939
|
for (const [backend, value] of Object.entries(parsed)) {
|
|
1712
1940
|
if (!backend.trim()) {
|
|
1713
|
-
throw new Error(
|
|
1941
|
+
throw new Error(
|
|
1942
|
+
"OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON contains an empty backend name",
|
|
1943
|
+
);
|
|
1714
1944
|
}
|
|
1715
1945
|
const rate = typeof value === "number" ? value : Number(value);
|
|
1716
1946
|
if (!Number.isFinite(rate) || rate < 0) {
|
|
1717
|
-
throw new Error(
|
|
1947
|
+
throw new Error(
|
|
1948
|
+
`OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON rate for ${backend} must be a non-negative number`,
|
|
1949
|
+
);
|
|
1718
1950
|
}
|
|
1719
1951
|
out[backend] = rate;
|
|
1720
1952
|
}
|
|
@@ -1742,7 +1974,9 @@ export function parseModelProvidersJson(raw: string): RegistryProvider[] {
|
|
|
1742
1974
|
parsed = JSON.parse(raw);
|
|
1743
1975
|
} catch (error) {
|
|
1744
1976
|
const message = error instanceof Error ? error.message : String(error);
|
|
1745
|
-
throw new Error(`OPENGENI_MODEL_PROVIDERS_JSON must be valid JSON: ${message}
|
|
1977
|
+
throw new Error(`OPENGENI_MODEL_PROVIDERS_JSON must be valid JSON: ${message}`, {
|
|
1978
|
+
cause: error,
|
|
1979
|
+
});
|
|
1746
1980
|
}
|
|
1747
1981
|
if (!Array.isArray(parsed)) {
|
|
1748
1982
|
throw new Error("OPENGENI_MODEL_PROVIDERS_JSON must be a JSON array of providers");
|
|
@@ -1750,13 +1984,17 @@ export function parseModelProvidersJson(raw: string): RegistryProvider[] {
|
|
|
1750
1984
|
return parsed.map((entry, index) => {
|
|
1751
1985
|
const result = RegistryProviderSchema.safeParse(entry);
|
|
1752
1986
|
if (!result.success) {
|
|
1753
|
-
throw new Error(
|
|
1987
|
+
throw new Error(
|
|
1988
|
+
`OPENGENI_MODEL_PROVIDERS_JSON provider[${index}] is invalid: ${result.error.message}`,
|
|
1989
|
+
);
|
|
1754
1990
|
}
|
|
1755
1991
|
return result.data;
|
|
1756
1992
|
});
|
|
1757
1993
|
}
|
|
1758
1994
|
|
|
1759
|
-
export function parseIntegrationsOauthClientsJson(
|
|
1995
|
+
export function parseIntegrationsOauthClientsJson(
|
|
1996
|
+
raw: string | undefined,
|
|
1997
|
+
): Record<string, IntegrationOAuthClientConfig> {
|
|
1760
1998
|
if (!raw?.trim() || raw.trim() === "{}") {
|
|
1761
1999
|
return {};
|
|
1762
2000
|
}
|
|
@@ -1765,10 +2003,14 @@ export function parseIntegrationsOauthClientsJson(raw: string | undefined): Reco
|
|
|
1765
2003
|
parsed = JSON.parse(raw);
|
|
1766
2004
|
} catch (error) {
|
|
1767
2005
|
const message = error instanceof Error ? error.message : String(error);
|
|
1768
|
-
throw new Error(`OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON must be valid JSON: ${message}
|
|
2006
|
+
throw new Error(`OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON must be valid JSON: ${message}`, {
|
|
2007
|
+
cause: error,
|
|
2008
|
+
});
|
|
1769
2009
|
}
|
|
1770
2010
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1771
|
-
throw new Error(
|
|
2011
|
+
throw new Error(
|
|
2012
|
+
"OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON must be a JSON object keyed by authorization-server issuer or URL",
|
|
2013
|
+
);
|
|
1772
2014
|
}
|
|
1773
2015
|
const out: Record<string, IntegrationOAuthClientConfig> = {};
|
|
1774
2016
|
for (const [key, value] of Object.entries(parsed)) {
|
|
@@ -1777,7 +2019,9 @@ export function parseIntegrationsOauthClientsJson(raw: string | undefined): Reco
|
|
|
1777
2019
|
}
|
|
1778
2020
|
const result = IntegrationOAuthClientConfigSchema.safeParse(value);
|
|
1779
2021
|
if (!result.success) {
|
|
1780
|
-
throw new Error(
|
|
2022
|
+
throw new Error(
|
|
2023
|
+
`OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON client for ${key} is invalid: ${result.error.message}`,
|
|
2024
|
+
);
|
|
1781
2025
|
}
|
|
1782
2026
|
out[key] = result.data;
|
|
1783
2027
|
}
|
|
@@ -1793,7 +2037,9 @@ export function parseStaticUsageLimitsJson(raw: string): StaticUsageLimitsConfig
|
|
|
1793
2037
|
parsed = JSON.parse(raw);
|
|
1794
2038
|
} catch (error) {
|
|
1795
2039
|
const message = error instanceof Error ? error.message : String(error);
|
|
1796
|
-
throw new Error(`OPENGENI_STATIC_USAGE_LIMITS_JSON must be valid JSON: ${message}
|
|
2040
|
+
throw new Error(`OPENGENI_STATIC_USAGE_LIMITS_JSON must be valid JSON: ${message}`, {
|
|
2041
|
+
cause: error,
|
|
2042
|
+
});
|
|
1797
2043
|
}
|
|
1798
2044
|
return StaticUsageLimits.parse(parsed);
|
|
1799
2045
|
}
|
|
@@ -1807,7 +2053,9 @@ export function parseStaticEntitlementsJson(raw: string): EntitlementsConfig {
|
|
|
1807
2053
|
parsed = JSON.parse(raw);
|
|
1808
2054
|
} catch (error) {
|
|
1809
2055
|
const message = error instanceof Error ? error.message : String(error);
|
|
1810
|
-
throw new Error(`OPENGENI_STATIC_ENTITLEMENTS_JSON must be valid JSON: ${message}
|
|
2056
|
+
throw new Error(`OPENGENI_STATIC_ENTITLEMENTS_JSON must be valid JSON: ${message}`, {
|
|
2057
|
+
cause: error,
|
|
2058
|
+
});
|
|
1811
2059
|
}
|
|
1812
2060
|
return Entitlements.parse(parsed);
|
|
1813
2061
|
}
|
|
@@ -1817,10 +2065,13 @@ function calculateEntryCostMicros(pricing: ModelPricing, entry: ModelUsageInput)
|
|
|
1817
2065
|
const outputTokens = positiveInt(entry.outputTokens);
|
|
1818
2066
|
const cachedTokens = Math.min(inputTokens, cachedInputTokens(entry));
|
|
1819
2067
|
const uncachedInputTokens = Math.max(0, inputTokens - cachedTokens);
|
|
1820
|
-
const cachedInputRate =
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2068
|
+
const cachedInputRate =
|
|
2069
|
+
pricing.cachedInputMicrosPerMillionTokens ?? pricing.inputMicrosPerMillionTokens;
|
|
2070
|
+
return (
|
|
2071
|
+
Math.ceil((uncachedInputTokens * pricing.inputMicrosPerMillionTokens) / 1_000_000) +
|
|
2072
|
+
Math.ceil((cachedTokens * cachedInputRate) / 1_000_000) +
|
|
2073
|
+
Math.ceil((outputTokens * pricing.outputMicrosPerMillionTokens) / 1_000_000)
|
|
2074
|
+
);
|
|
1824
2075
|
}
|
|
1825
2076
|
|
|
1826
2077
|
function cachedInputTokens(entry: ModelUsageInput): number {
|
|
@@ -1831,9 +2082,10 @@ function cachedInputTokens(entry: ModelUsageInput): number {
|
|
|
1831
2082
|
: [];
|
|
1832
2083
|
let total = 0;
|
|
1833
2084
|
for (const detail of details) {
|
|
1834
|
-
total +=
|
|
1835
|
-
|
|
1836
|
-
|
|
2085
|
+
total +=
|
|
2086
|
+
positiveInt(detail.cached_tokens) +
|
|
2087
|
+
positiveInt(detail.cachedInputTokens) +
|
|
2088
|
+
positiveInt(detail.cached_input_tokens);
|
|
1837
2089
|
}
|
|
1838
2090
|
return total;
|
|
1839
2091
|
}
|
|
@@ -1866,20 +2118,36 @@ function ensureBuiltInMcpServers(settings: Settings): Settings["mcpServers"] {
|
|
|
1866
2118
|
// safe to cache / leave as-is.)
|
|
1867
2119
|
cacheToolsList: false,
|
|
1868
2120
|
},
|
|
1869
|
-
...(hasFiles
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
2121
|
+
...(hasFiles
|
|
2122
|
+
? []
|
|
2123
|
+
: [
|
|
2124
|
+
{
|
|
2125
|
+
id: "files",
|
|
2126
|
+
name: "Files",
|
|
2127
|
+
url: firstPartyMcpUrl,
|
|
2128
|
+
allowedTools: ["files_get_download_url"],
|
|
2129
|
+
cacheToolsList: true,
|
|
2130
|
+
},
|
|
2131
|
+
]),
|
|
2132
|
+
...(hasDocs
|
|
2133
|
+
? []
|
|
2134
|
+
: [
|
|
2135
|
+
{
|
|
2136
|
+
id: "docs",
|
|
2137
|
+
name: "Document Search",
|
|
2138
|
+
url: firstPartyDocsMcpUrl,
|
|
2139
|
+
allowedTools: [
|
|
2140
|
+
"search_documents",
|
|
2141
|
+
"fetch_document_chunk",
|
|
2142
|
+
"list_document_bases",
|
|
2143
|
+
"knowledge_search",
|
|
2144
|
+
"knowledge_fetch",
|
|
2145
|
+
"memory_search",
|
|
2146
|
+
"memory_propose",
|
|
2147
|
+
],
|
|
2148
|
+
cacheToolsList: false,
|
|
2149
|
+
},
|
|
2150
|
+
]),
|
|
1883
2151
|
...existing,
|
|
1884
2152
|
];
|
|
1885
2153
|
}
|
|
@@ -1906,7 +2174,10 @@ function ensureBuiltInMcpServers(settings: Settings): Settings["mcpServers"] {
|
|
|
1906
2174
|
* the one binding a mounted embed cannot leave unset.
|
|
1907
2175
|
*/
|
|
1908
2176
|
export function firstPartyMcpBaseUrl(settings: Settings): string {
|
|
1909
|
-
return
|
|
2177
|
+
return (
|
|
2178
|
+
settings.opengeniMcpUrl ??
|
|
2179
|
+
`http://127.0.0.1:${settings.apiPort}/v1/workspaces/{workspaceId}/mcp`
|
|
2180
|
+
);
|
|
1910
2181
|
}
|
|
1911
2182
|
|
|
1912
2183
|
export function firstPartyMcpWorkspaceUrl(settings: Settings, workspaceId: string): string {
|
|
@@ -1935,45 +2206,67 @@ function validateSettings(settings: Settings): void {
|
|
|
1935
2206
|
}
|
|
1936
2207
|
if (settings.productAccessMode === "managed") {
|
|
1937
2208
|
if (!settings.publicBaseUrl) {
|
|
1938
|
-
throw new Error(
|
|
2209
|
+
throw new Error(
|
|
2210
|
+
"OPENGENI_PUBLIC_BASE_URL is required when OPENGENI_PRODUCT_ACCESS_MODE=managed",
|
|
2211
|
+
);
|
|
1939
2212
|
}
|
|
1940
2213
|
if (!settings.betterAuthSecret) {
|
|
1941
|
-
throw new Error(
|
|
2214
|
+
throw new Error(
|
|
2215
|
+
"OPENGENI_BETTER_AUTH_SECRET is required when OPENGENI_PRODUCT_ACCESS_MODE=managed",
|
|
2216
|
+
);
|
|
1942
2217
|
}
|
|
1943
2218
|
if (!settings.delegationSecret) {
|
|
1944
|
-
throw new Error(
|
|
2219
|
+
throw new Error(
|
|
2220
|
+
"OPENGENI_DELEGATION_SECRET is required when OPENGENI_PRODUCT_ACCESS_MODE=managed",
|
|
2221
|
+
);
|
|
1945
2222
|
}
|
|
1946
2223
|
if (!["local", "test"].includes(settings.environment) && !settings.resendApiKey) {
|
|
1947
2224
|
throw new Error("OPENGENI_RESEND_API_KEY is required for managed mode outside local/test");
|
|
1948
2225
|
}
|
|
1949
2226
|
if (!["local", "test"].includes(settings.environment) && !settings.environmentsEncryptionKey) {
|
|
1950
|
-
throw new Error(
|
|
2227
|
+
throw new Error(
|
|
2228
|
+
"OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is required for managed mode outside local/test",
|
|
2229
|
+
);
|
|
1951
2230
|
}
|
|
1952
2231
|
}
|
|
1953
2232
|
environmentsEncryptionKeyBytes(settings);
|
|
1954
2233
|
if (settings.integrationsEnabled) {
|
|
1955
2234
|
if (settings.productAccessMode === "managed" && !settings.publicBaseUrl) {
|
|
1956
|
-
throw new Error(
|
|
2235
|
+
throw new Error(
|
|
2236
|
+
"OPENGENI_PUBLIC_BASE_URL is required when OPENGENI_INTEGRATIONS_ENABLED=true and OPENGENI_PRODUCT_ACCESS_MODE=managed",
|
|
2237
|
+
);
|
|
1957
2238
|
}
|
|
1958
|
-
if (
|
|
1959
|
-
|
|
2239
|
+
if (
|
|
2240
|
+
settings.publicBaseUrl &&
|
|
2241
|
+
!settings.publicBaseUrl.startsWith("https://") &&
|
|
2242
|
+
!["local", "test"].includes(settings.environment)
|
|
2243
|
+
) {
|
|
2244
|
+
throw new Error(
|
|
2245
|
+
"OPENGENI_PUBLIC_BASE_URL must use https when OPENGENI_INTEGRATIONS_ENABLED=true outside local/test",
|
|
2246
|
+
);
|
|
1960
2247
|
}
|
|
1961
2248
|
if (!settings.integrationsStateSecret && !["local", "test"].includes(settings.environment)) {
|
|
1962
|
-
throw new Error(
|
|
2249
|
+
throw new Error(
|
|
2250
|
+
"OPENGENI_INTEGRATIONS_STATE_SECRET is required when OPENGENI_INTEGRATIONS_ENABLED=true outside local/test",
|
|
2251
|
+
);
|
|
1963
2252
|
}
|
|
1964
2253
|
}
|
|
1965
2254
|
parseIntegrationsOauthClientsJson(settings.integrationsOauthClientsJson);
|
|
1966
2255
|
if (
|
|
1967
|
-
settings.productAccessMode === "configured"
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
2256
|
+
settings.productAccessMode === "configured" &&
|
|
2257
|
+
!["local", "test"].includes(settings.environment) &&
|
|
2258
|
+
!settings.delegationSecret &&
|
|
2259
|
+
!settings.authRequired
|
|
1971
2260
|
) {
|
|
1972
|
-
throw new Error(
|
|
2261
|
+
throw new Error(
|
|
2262
|
+
"OPENGENI_PRODUCT_ACCESS_MODE=configured requires OPENGENI_DELEGATION_SECRET or OPENGENI_AUTH_REQUIRED=true outside local/test",
|
|
2263
|
+
);
|
|
1973
2264
|
}
|
|
1974
2265
|
if (settings.billingMode === "stripe") {
|
|
1975
2266
|
if (!settings.stripeSecretKey || !settings.stripeWebhookSecret) {
|
|
1976
|
-
throw new Error(
|
|
2267
|
+
throw new Error(
|
|
2268
|
+
"OPENGENI_STRIPE_SECRET_KEY and OPENGENI_STRIPE_WEBHOOK_SECRET are required when OPENGENI_BILLING_MODE=stripe",
|
|
2269
|
+
);
|
|
1977
2270
|
}
|
|
1978
2271
|
}
|
|
1979
2272
|
if (settings.productAccessMode !== "managed" && settings.billingMode === "stripe") {
|
|
@@ -1983,13 +2276,17 @@ function validateSettings(settings: Settings): void {
|
|
|
1983
2276
|
const pricing = configuredModelPricing(settings);
|
|
1984
2277
|
const missing = configuredAllowedModels(settings).filter((model) => !pricing[model]);
|
|
1985
2278
|
if (missing.length > 0) {
|
|
1986
|
-
throw new Error(
|
|
2279
|
+
throw new Error(
|
|
2280
|
+
`Missing model pricing for managed billing model(s): ${missing.join(", ")}. Set OPENGENI_MODEL_PRICING_JSON.`,
|
|
2281
|
+
);
|
|
1987
2282
|
}
|
|
1988
2283
|
}
|
|
1989
2284
|
if (settings.usageLimitsMode === "static") {
|
|
1990
2285
|
const limits = configuredStaticUsageLimits(settings);
|
|
1991
2286
|
if (Object.keys(limits).length === 0) {
|
|
1992
|
-
throw new Error(
|
|
2287
|
+
throw new Error(
|
|
2288
|
+
"OPENGENI_STATIC_USAGE_LIMITS_JSON must define at least one cap when OPENGENI_USAGE_LIMITS_MODE=static",
|
|
2289
|
+
);
|
|
1993
2290
|
}
|
|
1994
2291
|
} else {
|
|
1995
2292
|
parseStaticUsageLimitsJson(settings.staticUsageLimitsJson);
|
|
@@ -1997,7 +2294,9 @@ function validateSettings(settings: Settings): void {
|
|
|
1997
2294
|
if (settings.entitlementsMode === "static") {
|
|
1998
2295
|
const entitlements = parseStaticEntitlementsJson(settings.staticEntitlementsJson);
|
|
1999
2296
|
if (Object.keys(entitlements).length === 0) {
|
|
2000
|
-
throw new Error(
|
|
2297
|
+
throw new Error(
|
|
2298
|
+
"OPENGENI_STATIC_ENTITLEMENTS_JSON must define at least one feature when OPENGENI_ENTITLEMENTS_MODE=static",
|
|
2299
|
+
);
|
|
2001
2300
|
}
|
|
2002
2301
|
} else {
|
|
2003
2302
|
parseStaticEntitlementsJson(settings.staticEntitlementsJson);
|
|
@@ -2007,7 +2306,9 @@ function validateSettings(settings: Settings): void {
|
|
|
2007
2306
|
}
|
|
2008
2307
|
if (settings.openaiProvider === "azure") {
|
|
2009
2308
|
if (!settings.azureOpenaiBaseUrl && !settings.azureOpenaiEndpoint) {
|
|
2010
|
-
throw new Error(
|
|
2309
|
+
throw new Error(
|
|
2310
|
+
"Azure OpenAI requires OPENGENI_AZURE_OPENAI_BASE_URL or OPENGENI_AZURE_OPENAI_ENDPOINT",
|
|
2311
|
+
);
|
|
2011
2312
|
}
|
|
2012
2313
|
if (!settings.azureOpenaiBaseUrl && !settings.azureOpenaiDeployment) {
|
|
2013
2314
|
throw new Error("Azure OpenAI endpoint mode requires OPENGENI_AZURE_OPENAI_DEPLOYMENT");
|
|
@@ -2023,7 +2324,9 @@ function validateSettings(settings: Settings): void {
|
|
|
2023
2324
|
// (a half-configured token is always a misconfiguration). This is orthogonal
|
|
2024
2325
|
// to the backend-gated required-cred sweep below.
|
|
2025
2326
|
if (Boolean(settings.modalTokenId) !== Boolean(settings.modalTokenSecret)) {
|
|
2026
|
-
throw new Error(
|
|
2327
|
+
throw new Error(
|
|
2328
|
+
"OPENGENI_MODAL_TOKEN_ID and OPENGENI_MODAL_TOKEN_SECRET must both be set or both omitted",
|
|
2329
|
+
);
|
|
2027
2330
|
}
|
|
2028
2331
|
// Backend-gated required credentials: only the *active* backend's creds are
|
|
2029
2332
|
// required. A modal deployment must carry the Modal token; a daytona/e2b/none
|
|
@@ -2031,48 +2334,115 @@ function validateSettings(settings: Settings): void {
|
|
|
2031
2334
|
// SANDBOX_REQUIRED_ENV table that the deployment package also mirrors.
|
|
2032
2335
|
for (const required of SANDBOX_REQUIRED_ENV[settings.sandboxBackend] ?? []) {
|
|
2033
2336
|
const value = settings[required.field];
|
|
2034
|
-
if (
|
|
2035
|
-
|
|
2337
|
+
if (
|
|
2338
|
+
value === undefined ||
|
|
2339
|
+
value === null ||
|
|
2340
|
+
(typeof value === "string" && value.trim().length === 0)
|
|
2341
|
+
) {
|
|
2342
|
+
throw new Error(
|
|
2343
|
+
`${required.env} is required when OPENGENI_SANDBOX_BACKEND=${settings.sandboxBackend}`,
|
|
2344
|
+
);
|
|
2036
2345
|
}
|
|
2037
2346
|
}
|
|
2038
|
-
if (
|
|
2039
|
-
|
|
2040
|
-
|
|
2347
|
+
if (
|
|
2348
|
+
settings.objectStorageBackend === "s3-compatible" ||
|
|
2349
|
+
settings.objectStorageBackend === "aws-s3"
|
|
2350
|
+
) {
|
|
2351
|
+
if (
|
|
2352
|
+
Boolean(settings.objectStorageAccessKeyId) !== Boolean(settings.objectStorageSecretAccessKey)
|
|
2353
|
+
) {
|
|
2354
|
+
throw new Error(
|
|
2355
|
+
"OPENGENI_OBJECT_STORAGE_ACCESS_KEY_ID and OPENGENI_OBJECT_STORAGE_SECRET_ACCESS_KEY must both be set or both omitted",
|
|
2356
|
+
);
|
|
2041
2357
|
}
|
|
2042
|
-
if (
|
|
2043
|
-
|
|
2358
|
+
if (
|
|
2359
|
+
settings.objectStorageBackend === "s3-compatible" &&
|
|
2360
|
+
(settings.objectStorageEndpoint || settings.objectStorageSandboxEndpoint) &&
|
|
2361
|
+
(!settings.objectStorageAccessKeyId || !settings.objectStorageSecretAccessKey)
|
|
2362
|
+
) {
|
|
2363
|
+
throw new Error(
|
|
2364
|
+
"S3-compatible object storage endpoints require OPENGENI_OBJECT_STORAGE_ACCESS_KEY_ID and OPENGENI_OBJECT_STORAGE_SECRET_ACCESS_KEY",
|
|
2365
|
+
);
|
|
2044
2366
|
}
|
|
2045
|
-
if (
|
|
2046
|
-
|
|
2367
|
+
if (
|
|
2368
|
+
settings.objectStorageAzureConnectionString ||
|
|
2369
|
+
settings.objectStorageAzureAccountName ||
|
|
2370
|
+
settings.objectStorageAzureAccountKey ||
|
|
2371
|
+
settings.objectStorageAzureEndpoint
|
|
2372
|
+
) {
|
|
2373
|
+
throw new Error(
|
|
2374
|
+
"S3 object storage uses OPENGENI_OBJECT_STORAGE_* S3 settings, not OPENGENI_OBJECT_STORAGE_AZURE_* settings",
|
|
2375
|
+
);
|
|
2047
2376
|
}
|
|
2048
|
-
if (
|
|
2049
|
-
|
|
2377
|
+
if (
|
|
2378
|
+
settings.objectStorageGcsProjectId ||
|
|
2379
|
+
settings.objectStorageGcsCredentialsJson ||
|
|
2380
|
+
settings.objectStorageGcsKeyFilename ||
|
|
2381
|
+
settings.objectStorageGcsApiEndpoint
|
|
2382
|
+
) {
|
|
2383
|
+
throw new Error(
|
|
2384
|
+
"S3 object storage uses OPENGENI_OBJECT_STORAGE_* S3 settings, not OPENGENI_OBJECT_STORAGE_GCS_* settings",
|
|
2385
|
+
);
|
|
2050
2386
|
}
|
|
2051
2387
|
} else if (settings.objectStorageBackend === "azure-blob") {
|
|
2052
|
-
if (
|
|
2053
|
-
|
|
2388
|
+
if (
|
|
2389
|
+
settings.objectStorageEndpoint ||
|
|
2390
|
+
settings.objectStorageSandboxEndpoint ||
|
|
2391
|
+
settings.objectStorageAccessKeyId ||
|
|
2392
|
+
settings.objectStorageSecretAccessKey
|
|
2393
|
+
) {
|
|
2394
|
+
throw new Error(
|
|
2395
|
+
"Azure Blob storage uses OPENGENI_OBJECT_STORAGE_AZURE_* settings, not S3-compatible object storage settings",
|
|
2396
|
+
);
|
|
2054
2397
|
}
|
|
2055
|
-
if (
|
|
2056
|
-
|
|
2398
|
+
if (
|
|
2399
|
+
settings.objectStorageGcsProjectId ||
|
|
2400
|
+
settings.objectStorageGcsCredentialsJson ||
|
|
2401
|
+
settings.objectStorageGcsKeyFilename ||
|
|
2402
|
+
settings.objectStorageGcsApiEndpoint
|
|
2403
|
+
) {
|
|
2404
|
+
throw new Error(
|
|
2405
|
+
"Azure Blob storage uses OPENGENI_OBJECT_STORAGE_AZURE_* settings, not OPENGENI_OBJECT_STORAGE_GCS_* settings",
|
|
2406
|
+
);
|
|
2057
2407
|
}
|
|
2058
2408
|
const hasConnectionString = Boolean(settings.objectStorageAzureConnectionString);
|
|
2059
|
-
const hasSharedKey =
|
|
2409
|
+
const hasSharedKey =
|
|
2410
|
+
Boolean(settings.objectStorageAzureAccountName) &&
|
|
2411
|
+
Boolean(settings.objectStorageAzureAccountKey);
|
|
2060
2412
|
if (!hasConnectionString && !hasSharedKey) {
|
|
2061
|
-
throw new Error(
|
|
2413
|
+
throw new Error(
|
|
2414
|
+
"Azure Blob storage requires OPENGENI_OBJECT_STORAGE_AZURE_CONNECTION_STRING or OPENGENI_OBJECT_STORAGE_AZURE_ACCOUNT_NAME plus OPENGENI_OBJECT_STORAGE_AZURE_ACCOUNT_KEY",
|
|
2415
|
+
);
|
|
2062
2416
|
}
|
|
2063
2417
|
} else {
|
|
2064
|
-
if (
|
|
2065
|
-
|
|
2418
|
+
if (
|
|
2419
|
+
settings.objectStorageEndpoint ||
|
|
2420
|
+
settings.objectStorageSandboxEndpoint ||
|
|
2421
|
+
settings.objectStorageAccessKeyId ||
|
|
2422
|
+
settings.objectStorageSecretAccessKey
|
|
2423
|
+
) {
|
|
2424
|
+
throw new Error(
|
|
2425
|
+
"GCS object storage uses OPENGENI_OBJECT_STORAGE_GCS_* settings, not S3-compatible object storage settings",
|
|
2426
|
+
);
|
|
2066
2427
|
}
|
|
2067
|
-
if (
|
|
2068
|
-
|
|
2428
|
+
if (
|
|
2429
|
+
settings.objectStorageAzureConnectionString ||
|
|
2430
|
+
settings.objectStorageAzureAccountName ||
|
|
2431
|
+
settings.objectStorageAzureAccountKey ||
|
|
2432
|
+
settings.objectStorageAzureEndpoint
|
|
2433
|
+
) {
|
|
2434
|
+
throw new Error(
|
|
2435
|
+
"GCS object storage uses OPENGENI_OBJECT_STORAGE_GCS_* settings, not OPENGENI_OBJECT_STORAGE_AZURE_* settings",
|
|
2436
|
+
);
|
|
2069
2437
|
}
|
|
2070
2438
|
if (settings.objectStorageGcsCredentialsJson) {
|
|
2071
2439
|
parseGcsCredentialsJson(settings.objectStorageGcsCredentialsJson);
|
|
2072
2440
|
}
|
|
2073
2441
|
}
|
|
2074
2442
|
if (settings.documentChunkOverlap >= settings.documentChunkSize) {
|
|
2075
|
-
throw new Error(
|
|
2443
|
+
throw new Error(
|
|
2444
|
+
"OPENGENI_DOCUMENT_CHUNK_OVERLAP must be smaller than OPENGENI_DOCUMENT_CHUNK_SIZE",
|
|
2445
|
+
);
|
|
2076
2446
|
}
|
|
2077
2447
|
parseExposedPorts(settings.dockerExposedPorts);
|
|
2078
2448
|
sandboxEnvironmentVariableNames(settings);
|
|
@@ -2111,31 +2481,35 @@ function validateSettings(settings: Settings): void {
|
|
|
2111
2481
|
const idleTimeoutMs = effectiveModalIdleTimeoutSeconds(settings) * 1000;
|
|
2112
2482
|
if (!(reaperPeriod < viewerTtl)) {
|
|
2113
2483
|
throw new Error(
|
|
2114
|
-
`OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS (${reaperPeriod}) must be strictly less than `
|
|
2115
|
-
|
|
2116
|
-
|
|
2484
|
+
`OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS (${reaperPeriod}) must be strictly less than ` +
|
|
2485
|
+
`OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS (${viewerTtl}): the reaper must run more often ` +
|
|
2486
|
+
`than the TTL it polices, or stale viewer holders outlive a full reaper period.`,
|
|
2487
|
+
);
|
|
2117
2488
|
}
|
|
2118
2489
|
if (!(idleTimeoutMs <= providerLifetimeMs)) {
|
|
2119
2490
|
throw new Error(
|
|
2120
|
-
`OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS*1000 (${idleTimeoutMs}) must not exceed the hard provider `
|
|
2121
|
-
|
|
2122
|
-
|
|
2491
|
+
`OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS*1000 (${idleTimeoutMs}) must not exceed the hard provider ` +
|
|
2492
|
+
`lifetime (OPENGENI_MODAL_TIMEOUT_SECONDS*1000 = ${providerLifetimeMs}): the idle timeout is a ` +
|
|
2493
|
+
`floor under the hard lifetime, not above it.`,
|
|
2494
|
+
);
|
|
2123
2495
|
}
|
|
2124
2496
|
if (!(viewerTtl < idleTimeoutMs)) {
|
|
2125
2497
|
throw new Error(
|
|
2126
|
-
`OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS (${viewerTtl}) must be strictly less than the effective box `
|
|
2127
|
-
|
|
2128
|
-
|
|
2498
|
+
`OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS (${viewerTtl}) must be strictly less than the effective box ` +
|
|
2499
|
+
`idle timeout (${idleTimeoutMs}): a viewer holder must be reapable before the box idles out from ` +
|
|
2500
|
+
`under it (the provider idle-timeout is the backstop).`,
|
|
2501
|
+
);
|
|
2129
2502
|
}
|
|
2130
2503
|
if (!(reaperPeriod + idleGraceMs < idleTimeoutMs)) {
|
|
2131
2504
|
throw new Error(
|
|
2132
|
-
`OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS + OPENGENI_SANDBOX_IDLE_GRACE_MS `
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2505
|
+
`OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS + OPENGENI_SANDBOX_IDLE_GRACE_MS ` +
|
|
2506
|
+
`(${reaperPeriod} + ${idleGraceMs} = ${reaperPeriod + idleGraceMs}) must be strictly less than the ` +
|
|
2507
|
+
`effective box idle timeout (${idleTimeoutMs}): a drained box must SURVIVE its full warm window so ` +
|
|
2508
|
+
`the reaper can resume + snapshot /workspace + terminate it on the sweep AFTER the drain grace ` +
|
|
2509
|
+
`elapses — Modal's idle-reap must NOT fire first (or /workspace is lost). Raise ` +
|
|
2510
|
+
`OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS (defaults to OPENGENI_MODAL_TIMEOUT_SECONDS) or lower ` +
|
|
2511
|
+
`OPENGENI_SANDBOX_IDLE_GRACE_MS.`,
|
|
2512
|
+
);
|
|
2139
2513
|
}
|
|
2140
2514
|
}
|
|
2141
2515
|
// --- stream-token secret: required-when-desktop, but GRACEFULLY DEGRADE (I8) ---
|
|
@@ -2148,10 +2522,10 @@ function validateSettings(settings: Settings): void {
|
|
|
2148
2522
|
// crashing the whole API on a missing secret.
|
|
2149
2523
|
if (settings.sandboxDesktopEnabled && resolveStreamTokenSecret(settings) === undefined) {
|
|
2150
2524
|
console.warn(
|
|
2151
|
-
"[opengeni] OPENGENI_SANDBOX_DESKTOP_ENABLED=true but neither OPENGENI_STREAM_TOKEN_SECRET nor "
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2525
|
+
"[opengeni] OPENGENI_SANDBOX_DESKTOP_ENABLED=true but neither OPENGENI_STREAM_TOKEN_SECRET nor " +
|
|
2526
|
+
"OPENGENI_DELEGATION_SECRET is set: the desktop pixel plane will GRACEFULLY DEGRADE " +
|
|
2527
|
+
"(DesktopStream.transport=null — no scoped stream tokens can be minted). Set " +
|
|
2528
|
+
"OPENGENI_STREAM_TOKEN_SECRET to enable the live desktop stream.",
|
|
2155
2529
|
);
|
|
2156
2530
|
}
|
|
2157
2531
|
// Model provider registry: parse it here so JSON/zod errors surface at boot,
|
|
@@ -2166,14 +2540,20 @@ function validateSettings(settings: Settings): void {
|
|
|
2166
2540
|
const providerIds = new Set<string>();
|
|
2167
2541
|
for (const provider of registryProviders) {
|
|
2168
2542
|
if (provider.id === builtinId) {
|
|
2169
|
-
throw new Error(
|
|
2543
|
+
throw new Error(
|
|
2544
|
+
`OPENGENI_MODEL_PROVIDERS_JSON provider id ${provider.id} collides with the built-in provider id`,
|
|
2545
|
+
);
|
|
2170
2546
|
}
|
|
2171
2547
|
if (providerIds.has(provider.id)) {
|
|
2172
|
-
throw new Error(
|
|
2548
|
+
throw new Error(
|
|
2549
|
+
`OPENGENI_MODEL_PROVIDERS_JSON contains duplicate provider id ${provider.id}`,
|
|
2550
|
+
);
|
|
2173
2551
|
}
|
|
2174
2552
|
providerIds.add(provider.id);
|
|
2175
2553
|
if (!resolveProviderApiKey(provider)) {
|
|
2176
|
-
throw new Error(
|
|
2554
|
+
throw new Error(
|
|
2555
|
+
`OPENGENI_MODEL_PROVIDERS_JSON provider ${provider.id} requires a resolvable API key (set apiKey or apiKeyEnv)`,
|
|
2556
|
+
);
|
|
2177
2557
|
}
|
|
2178
2558
|
}
|
|
2179
2559
|
}
|
|
@@ -2293,7 +2673,10 @@ export function resolveNatsControlPlaneAuth(settings: Settings): NatsControlPlan
|
|
|
2293
2673
|
}
|
|
2294
2674
|
|
|
2295
2675
|
function splitCsv(raw: string): string[] {
|
|
2296
|
-
return raw
|
|
2676
|
+
return raw
|
|
2677
|
+
.split(",")
|
|
2678
|
+
.map((value) => value.trim())
|
|
2679
|
+
.filter(Boolean);
|
|
2297
2680
|
}
|
|
2298
2681
|
|
|
2299
2682
|
function uniqueEnvNames(raw: string[], fieldName: string): string[] {
|
|
@@ -2320,7 +2703,9 @@ function parseGcsCredentialsJson(raw: string): unknown {
|
|
|
2320
2703
|
return JSON.parse(raw);
|
|
2321
2704
|
} catch (error) {
|
|
2322
2705
|
const message = error instanceof Error ? error.message : String(error);
|
|
2323
|
-
throw new Error(`OPENGENI_OBJECT_STORAGE_GCS_CREDENTIALS_JSON must be valid JSON: ${message}
|
|
2706
|
+
throw new Error(`OPENGENI_OBJECT_STORAGE_GCS_CREDENTIALS_JSON must be valid JSON: ${message}`, {
|
|
2707
|
+
cause: error,
|
|
2708
|
+
});
|
|
2324
2709
|
}
|
|
2325
2710
|
}
|
|
2326
2711
|
|