@opengeni/config 0.4.0 → 0.5.1
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 +77 -58
- package/dist/index.js +410 -204
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
- package/src/index.ts +710 -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"),
|
|
@@ -181,57 +186,36 @@ const SettingsSchema = z.object({
|
|
|
181
186
|
// above; the graceful max-turns valve (idle + goal continuation, never a
|
|
182
187
|
// session failure) remains as inert safety should a deployment set a cap.
|
|
183
188
|
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.
|
|
189
|
+
// The model family's real context window in tokens. OpenGeni always performs
|
|
190
|
+
// one durable, portable plaintext compaction transition; there is no
|
|
191
|
+
// provider/server/off mode ladder.
|
|
207
192
|
contextWindowTokens: z.coerce.number().int().positive().default(1_050_000),
|
|
193
|
+
// Optional model-catalog effective input ceiling. Codex models expose this as
|
|
194
|
+
// raw context_window * effective_context_window_percent; when absent, retain
|
|
195
|
+
// the deployment-level window-minus-reserved-output behavior.
|
|
196
|
+
contextEffectiveWindowTokens: z.coerce.number().int().positive().optional(),
|
|
208
197
|
// Proactive compaction threshold as a ratio of the model context window.
|
|
209
|
-
// Defaults to
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
198
|
+
// Defaults to 90%: compact as late as possible — retained context beats early
|
|
199
|
+
// headroom now that per-model windows are declared honestly (input-effective,
|
|
200
|
+
// empirically measured), and the fail-closed reactive compact-on-reject path
|
|
201
|
+
// absorbs any overshoot as one retried call rather than a dead session.
|
|
202
|
+
// Clamped to [0.3, 0.9] so deployments can tune the trigger without
|
|
203
|
+
// accidentally disabling compaction.
|
|
204
|
+
contextCompactionThresholdRatio: z.coerce
|
|
205
|
+
.number()
|
|
206
|
+
.default(0.9)
|
|
207
|
+
.transform((value) => {
|
|
208
|
+
if (!Number.isFinite(value)) {
|
|
209
|
+
return 0.9;
|
|
210
|
+
}
|
|
211
|
+
return Math.min(0.9, Math.max(0.3, value));
|
|
212
|
+
}),
|
|
218
213
|
// Tokens reserved for model output; subtracted from the window to get the
|
|
219
214
|
// usable input budget B = contextWindowTokens - contextReservedOutputTokens.
|
|
220
215
|
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),
|
|
216
|
+
// Model-catalog auto-compact limit. When present it is clamped to
|
|
217
|
+
// 90% of the raw window, matching Codex core's auto_compact_token_limit().
|
|
218
|
+
contextAutoCompactThresholdTokens: z.coerce.number().int().positive().optional(),
|
|
235
219
|
authRequired: EnvBoolean.default(false),
|
|
236
220
|
accessKey: z.string().optional(),
|
|
237
221
|
authAllowHealth: EnvBoolean.default(true),
|
|
@@ -244,8 +228,8 @@ const SettingsSchema = z.object({
|
|
|
244
228
|
openaiProvider: z.enum(["openai", "azure"]).default("openai"),
|
|
245
229
|
openaiApiKey: z.string().optional(),
|
|
246
230
|
openaiBaseUrl: z.string().optional(),
|
|
247
|
-
openaiModel: z.string().default("gpt-5.
|
|
248
|
-
openaiAllowedModels: z.string().default("gpt-5.
|
|
231
|
+
openaiModel: z.string().default("gpt-5.6-sol"),
|
|
232
|
+
openaiAllowedModels: z.string().default("gpt-5.6-sol,gpt-5.6-terra,gpt-5.6-luna"),
|
|
249
233
|
modelPricingJson: z.string().default("{}"),
|
|
250
234
|
// Extra (non-built-in) model providers, declared by the host as a JSON
|
|
251
235
|
// provider registry. Each entry carries its own base URL, API key, wire API
|
|
@@ -257,14 +241,19 @@ const SettingsSchema = z.object({
|
|
|
257
241
|
// Codex (ChatGPT) subscription: when enabled, a per-workspace connected
|
|
258
242
|
// subscription is injected as a synthetic "codex-subscription" registry
|
|
259
243
|
// provider whose models route through the ChatGPT backend (@opengeni/codex).
|
|
260
|
-
codexSubscriptionEnabled: EnvBoolean.default(false),
|
|
261
|
-
codexProductSku: z.string().optional(),
|
|
244
|
+
codexSubscriptionEnabled: EnvBoolean.default(false), // OPENGENI_CODEX_SUBSCRIPTION_ENABLED
|
|
245
|
+
codexProductSku: z.string().optional(), // OPENGENI_CODEX_PRODUCT_SKU (X-OpenAI-Product-Sku, apps only)
|
|
262
246
|
// Progressive connector disclosure (Codex-CLI-style tool_search): on a codex
|
|
263
247
|
// turn, flag the ~217 codex_apps connector tools `defer_loading:true` (dropping
|
|
264
248
|
// their schemas from model context) and add one client-executed tool_search
|
|
265
249
|
// tool that BM25-discloses only the matching connectors. Default OFF — a codex
|
|
266
250
|
// turn is byte-for-byte unchanged until enabled. OPENGENI_CODEX_TOOL_SEARCH_ENABLED
|
|
267
251
|
codexToolSearchEnabled: EnvBoolean.default(false),
|
|
252
|
+
// OPE-21 atomic, workspace-local credential allocation. Default OFF is a
|
|
253
|
+
// deliberate rolling-deploy fence: migrate + roll every worker first, then
|
|
254
|
+
// enable. Turning it off restores the legacy sticky selector without a schema
|
|
255
|
+
// rollback; the additive lease table/cursor columns become inert.
|
|
256
|
+
codexCredentialLeasingEnabled: EnvBoolean.default(false),
|
|
268
257
|
// Multi-account P3 (auto-rotation): an account is "near exhaustion" — ineligible to be
|
|
269
258
|
// rotated TO — when EITHER usage window (5h/weekly) is at/over this percent. Default 90 to
|
|
270
259
|
// match the UI danger flip (UsageBar danger at pct >= 90). OPENGENI_CODEX_ROTATION_NEAR_EXHAUSTION_PCT.
|
|
@@ -302,7 +291,7 @@ const SettingsSchema = z.object({
|
|
|
302
291
|
// the non-bypassable CORE at AGENT_INSTRUCTIONS_CORE_PLACEHOLDER (or appends
|
|
303
292
|
// it when the template omits the marker), and uses the result as the agent's
|
|
304
293
|
// instructions. Defaulting to DEFAULT_AGENT_INSTRUCTIONS keeps the composed
|
|
305
|
-
// default
|
|
294
|
+
// default pinned by runtime tests.
|
|
306
295
|
agentInstructionsTemplate: z.string().default(DEFAULT_AGENT_INSTRUCTIONS),
|
|
307
296
|
azureOpenaiBaseUrl: z.string().optional(),
|
|
308
297
|
azureOpenaiEndpoint: z.string().optional(),
|
|
@@ -409,6 +398,13 @@ const SettingsSchema = z.object({
|
|
|
409
398
|
// recordingMaxSeconds is the ffmpeg -t hard ceiling (bounds a multi-day turn).
|
|
410
399
|
recordingEnabled: EnvBoolean.default(true),
|
|
411
400
|
recordingDefaultCodec: z.enum(["h264-mp4", "vp9-webm"]).default("h264-mp4"),
|
|
401
|
+
// Workbench v2 turn-end workspace capture (dossier §10.1). When on, the turn
|
|
402
|
+
// activity probes the box's changed files off the live box at turn end and
|
|
403
|
+
// persists a capture revision (blobs in @opengeni/storage) so the workbench
|
|
404
|
+
// paints cold/offline sessions with zero machine round-trips. Best-effort and
|
|
405
|
+
// fully behind this flag: off ⇒ capture is skipped and reads fall back to the
|
|
406
|
+
// live/wake path (status-quo behavior). Default on; explicit per environment.
|
|
407
|
+
workspaceCaptureEnabled: EnvBoolean.default(true),
|
|
412
408
|
recordingFramerate: z.coerce.number().int().positive().default(15),
|
|
413
409
|
recordingMaxSeconds: z.coerce.number().int().positive().default(600),
|
|
414
410
|
recordingMaxBytes: z.coerce.number().int().positive().default(268_435_456), // 256 MB
|
|
@@ -462,6 +458,19 @@ const SettingsSchema = z.object({
|
|
|
462
458
|
// EnvBoolean (NOT z.coerce.boolean(), which would coerce "false" -> true and
|
|
463
459
|
// turn the flag ON the moment anyone set the env var to disable it).
|
|
464
460
|
sandboxOwnershipEnabled: EnvBoolean.default(false),
|
|
461
|
+
// --- lazy sandbox provisioning rollout flag, default OFF ---
|
|
462
|
+
// Only effective when sandboxOwnershipEnabled is ALSO on (lazy provisioning is a
|
|
463
|
+
// property of the owned path — the SDK never creates/resumes an injected session,
|
|
464
|
+
// so we control when the box is established). When TRUE, a turn does NOT provision
|
|
465
|
+
// its box at turn start: the lease acquire + resume-by-id + hooks + downloads +
|
|
466
|
+
// heartbeat + recording are deferred to an in-process single-flight provisioner
|
|
467
|
+
// that runs the FIRST time a sandbox op is dispatched (via the routing proxy's
|
|
468
|
+
// resolveActiveBackend). A turn whose model never calls a sandbox-backed tool ends
|
|
469
|
+
// with NO lease row and ZERO warm-seconds. When FALSE (or ownership off) the turn
|
|
470
|
+
// provisions eagerly exactly as today — byte-for-byte. EnvBoolean (NOT
|
|
471
|
+
// z.coerce.boolean(), which coerces "false" -> true and would turn the flag ON the
|
|
472
|
+
// moment anyone set the env var to disable it).
|
|
473
|
+
sandboxLazyProvisionEnabled: EnvBoolean.default(false),
|
|
465
474
|
// --- bring-your-own-compute (selfhosted 11th backend) rollout flag, default OFF ---
|
|
466
475
|
// The keystone flag for the whole selfhosted feature (the enrollment device-flow,
|
|
467
476
|
// the NATS control plane, the relay stream tier). When FALSE the enrollment routes
|
|
@@ -470,6 +479,11 @@ const SettingsSchema = z.object({
|
|
|
470
479
|
// z.coerce.boolean(), which coerces "false" -> true). Flipped per-environment via
|
|
471
480
|
// the deploy-staging IaC secret/configmap pattern (dossier §17/§25.1).
|
|
472
481
|
sandboxSelfhostedEnabled: EnvBoolean.default(false),
|
|
482
|
+
// Gates the op-stream (streaming exec) transport to Connected Machines. The
|
|
483
|
+
// runner must ALSO advertise Capabilities.op_stream; default off, and legacy
|
|
484
|
+
// request/reply exec is the permanent fallback. EnvBoolean (NOT
|
|
485
|
+
// z.coerce.boolean(), which coerces "false" -> true).
|
|
486
|
+
agentOpStreamEnabled: EnvBoolean.default(false),
|
|
473
487
|
// The HMAC secret the control plane signs the enrollment bearer credential with
|
|
474
488
|
// (the `oge_` envelope the agent presents back to the control plane). Optional:
|
|
475
489
|
// when ABSENT and sandboxSelfhostedEnabled is on, the poll route reports the
|
|
@@ -526,6 +540,25 @@ const SettingsSchema = z.object({
|
|
|
526
540
|
// bus connects anonymously (local dev / a NATS with no auth_callout).
|
|
527
541
|
selfhostedNatsControlUser: z.string().optional(),
|
|
528
542
|
selfhostedNatsControlPassword: z.string().optional(),
|
|
543
|
+
// --- selfhosted (Connected Machine) control/exec op deadlines ---------------
|
|
544
|
+
// The control plane splits its op deadline in two. CONTROL ops (ping / fs / git /
|
|
545
|
+
// desktop / pty) must stay responsive so a machine's liveness is never masked by a
|
|
546
|
+
// slow op, so they use the short control timeout. EXEC gets its OWN, larger budget:
|
|
547
|
+
// a real command (compile, test run, dependency install) routinely outlives the
|
|
548
|
+
// control timeout, and before the split a long command was killed at the ~30s
|
|
549
|
+
// control wall. The agent kills the exec child at this deadline; the wire waits
|
|
550
|
+
// slightly longer (SELFHOSTED_EXEC_REPLY_GRACE_MS) for the typed timed-out reply.
|
|
551
|
+
//
|
|
552
|
+
// The exec default is a DELIBERATELY MODEST 2min (not 5): the agent-side admission
|
|
553
|
+
// pool is (until a later agent release) a FLAT 8 permits with no per-class split,
|
|
554
|
+
// so 8 slow execs holding a permit for 5 minutes would blanket-DRAIN every fs/git
|
|
555
|
+
// op — shipping the amplifier before the class-aware-admission fix. 2min still
|
|
556
|
+
// clears the large majority of the observed >30s exec tail; genuinely long jobs run
|
|
557
|
+
// in the background (see the exec-deadline hint) or raise the knob per deployment.
|
|
558
|
+
// Knobs: OPENGENI_SANDBOX_SELFHOSTED_EXEC_TIMEOUT_MS (default 2min) and
|
|
559
|
+
// OPENGENI_SANDBOX_SELFHOSTED_CONTROL_TIMEOUT_MS (default 30s).
|
|
560
|
+
sandboxSelfhostedExecTimeoutMs: z.coerce.number().int().positive().default(120_000),
|
|
561
|
+
sandboxSelfhostedControlTimeoutMs: z.coerce.number().int().positive().default(30_000),
|
|
529
562
|
// --- sandbox lease cadences (cadence invariant validated at boot below) ---
|
|
530
563
|
// reaperPeriod < viewerHolderTTL, and reaperPeriod + idleGrace < the EFFECTIVE
|
|
531
564
|
// box idle timeout (effectiveModalIdleTimeoutSeconds, which defaults to the hard
|
|
@@ -544,6 +577,22 @@ const SettingsSchema = z.object({
|
|
|
544
577
|
// fresh EMPTY box; lower it to trade warm cost for a snappier reclaim. Knob:
|
|
545
578
|
// OPENGENI_SANDBOX_IDLE_GRACE_MS.
|
|
546
579
|
sandboxIdleGraceMs: z.coerce.number().int().positive().default(900_000),
|
|
580
|
+
// MID-SESSION /workspace snapshot cadence (sandbox-file-persistence). The
|
|
581
|
+
// reaper's drain-persist only protects boxes the reaper itself kills; a box
|
|
582
|
+
// that dies any other way (Modal's hard creation-time timeout on a session
|
|
583
|
+
// busy past it, provider OOM/infra death) loses everything since the last
|
|
584
|
+
// clean drain. While a turn holds the box, the turn heartbeat and turn-end
|
|
585
|
+
// both take a snapshot when at least this interval has passed since the last
|
|
586
|
+
// one (same epoch-fenced fold-onto-lease seam as the drain), bounding the
|
|
587
|
+
// worst-case loss of ANY unclean box death to this window. 0 disables.
|
|
588
|
+
// Knob: OPENGENI_SANDBOX_SNAPSHOT_INTERVAL_MS. Default 15min.
|
|
589
|
+
sandboxSnapshotIntervalMs: z.coerce.number().int().min(0).default(900_000),
|
|
590
|
+
// Maximum time a best-effort /workspace snapshot capture may hold turn/reaper
|
|
591
|
+
// cleanup. A hung provider snapshot must never pin a lease holder, block
|
|
592
|
+
// graceful shutdown, or become permission to GC an older archive. Timeout is
|
|
593
|
+
// treated exactly like a failed best-effort snapshot. Knob:
|
|
594
|
+
// OPENGENI_SANDBOX_SNAPSHOT_TIMEOUT_MS. Default 60s.
|
|
595
|
+
sandboxSnapshotTimeoutMs: z.coerce.number().int().positive().default(60_000),
|
|
547
596
|
// expires_at refresh window for a held lease (>> the turn 10s heartbeat so a
|
|
548
597
|
// single missed heartbeat never TTL-reaps a live turn). The warming TTL is the
|
|
549
598
|
// window a cold->warming spawner has to commit warm before a reaper resets it.
|
|
@@ -553,6 +602,11 @@ const SettingsSchema = z.object({
|
|
|
553
602
|
// (a liveness/reaper cadence), this bounds how long one turn waits for capacity
|
|
554
603
|
// or provider creation before surfacing a clear turn.failed error.
|
|
555
604
|
sandboxWarmingTimeoutMs: z.coerce.number().int().positive().default(600_000),
|
|
605
|
+
// Rig setup-script budget (M3): the wall-clock timeout the rig-setup lifecycle
|
|
606
|
+
// hook runs its script under, distinct from the 120s per-command lifecycle
|
|
607
|
+
// default (a rig may compile/install heavy tooling on first cold create).
|
|
608
|
+
// Env: OPENGENI_RIG_SETUP_TIMEOUT_MS. Default 10min.
|
|
609
|
+
rigSetupTimeoutMs: z.coerce.number().int().positive().default(600_000),
|
|
556
610
|
// --- sandbox warm-time billing (P2.1) ---
|
|
557
611
|
// Per-backend warm rate (usd_micros/sec), like modelPricingJson: an empty {}
|
|
558
612
|
// means warm-cost is not debited (warm-seconds are still metered for audit).
|
|
@@ -568,7 +622,9 @@ const SettingsSchema = z.object({
|
|
|
568
622
|
sandboxEnvAllowlist: z.string().default(""),
|
|
569
623
|
objectStorageEndpoint: z.string().url().optional(),
|
|
570
624
|
objectStorageSandboxEndpoint: z.string().url().optional(),
|
|
571
|
-
objectStorageBackend: z
|
|
625
|
+
objectStorageBackend: z
|
|
626
|
+
.enum(["s3-compatible", "aws-s3", "azure-blob", "gcs"])
|
|
627
|
+
.default("s3-compatible"),
|
|
572
628
|
objectStorageBucket: z.string().min(1).default("opengeni-files"),
|
|
573
629
|
objectStorageRegion: z.string().min(1).default("us-east-1"),
|
|
574
630
|
objectStorageS3Provider: z.string().min(1).default("Minio"),
|
|
@@ -613,30 +669,34 @@ const SettingsSchema = z.object({
|
|
|
613
669
|
stripePublishableKey: z.string().optional(),
|
|
614
670
|
stripeWebhookSecret: z.string().optional(),
|
|
615
671
|
stripeCreditsProductId: z.string().optional(),
|
|
616
|
-
mcpServers: z
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
672
|
+
mcpServers: z
|
|
673
|
+
.array(
|
|
674
|
+
z.object({
|
|
675
|
+
id: z.string().min(1).regex(registryId),
|
|
676
|
+
name: z.string().min(1).optional(),
|
|
677
|
+
url: z.string().url(),
|
|
678
|
+
allowedTools: z.array(z.string().min(1)).optional(),
|
|
679
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
680
|
+
cacheToolsList: z.boolean().default(false),
|
|
681
|
+
/**
|
|
682
|
+
* Human-approval policy for this server's tools, overlaid per-run from a
|
|
683
|
+
* session MCP server row (never from OPENGENI_MCP_SERVERS). `true` = all
|
|
684
|
+
* tools require approval; a string[] = only the listed UNPREFIXED tool
|
|
685
|
+
* names do; absent = auto-run (the historical default). Enforced in the
|
|
686
|
+
* runtime by attaching `needsApproval` to the matching MCP tools.
|
|
687
|
+
*/
|
|
688
|
+
requireApproval: z.union([z.boolean(), z.array(z.string().min(1))]).optional(),
|
|
689
|
+
/**
|
|
690
|
+
* Extra request headers sent to this MCP server (credential injection
|
|
691
|
+
* for workspace-enabled capability MCPs). Populated at runtime from
|
|
692
|
+
* encrypted capability-installation credentials; do not put secrets in
|
|
693
|
+
* OPENGENI_MCP_SERVERS.
|
|
694
|
+
*/
|
|
695
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
696
|
+
connectionRef: McpServerConnectionRefSchema.optional(),
|
|
697
|
+
}),
|
|
698
|
+
)
|
|
699
|
+
.default([]),
|
|
640
700
|
});
|
|
641
701
|
|
|
642
702
|
export type Settings = z.infer<typeof SettingsSchema>;
|
|
@@ -685,23 +745,25 @@ export type RegistryProviderKind = z.infer<typeof RegistryProviderKind>;
|
|
|
685
745
|
|
|
686
746
|
/** A single model exposed by a registry provider. */
|
|
687
747
|
const RegistryModelSchema = z.object({
|
|
688
|
-
id: z.string().min(1),
|
|
689
|
-
label: z.string().min(1).optional(),
|
|
748
|
+
id: z.string().min(1), // model id sent to the provider, e.g. "accounts/fireworks/models/glm-5p2"
|
|
749
|
+
label: z.string().min(1).optional(), // display name; defaults to id
|
|
690
750
|
contextWindowTokens: z.number().int().positive().optional(),
|
|
691
|
-
|
|
692
|
-
|
|
751
|
+
effectiveContextWindowTokens: z.number().int().positive().optional(),
|
|
752
|
+
autoCompactTokenLimit: z.number().int().positive().optional(),
|
|
753
|
+
reasoningEffort: z.boolean().optional(), // model accepts a reasoning-effort control
|
|
754
|
+
hostedWebSearch: z.boolean().optional(), // provider executes the hosted web_search tool for this model
|
|
693
755
|
pricing: ModelPricingSchema.optional(),
|
|
694
756
|
});
|
|
695
757
|
|
|
696
758
|
/** A non-built-in provider declared by the host via OPENGENI_MODEL_PROVIDERS_JSON. */
|
|
697
759
|
const RegistryProviderSchema = z.object({
|
|
698
|
-
kind: RegistryProviderKind.default("api-key"),
|
|
699
|
-
id: z.string().min(1).regex(registryId),
|
|
760
|
+
kind: RegistryProviderKind.default("api-key"), // "codex-subscription" => per-request token, no static key
|
|
761
|
+
id: z.string().min(1).regex(registryId), // stable provider id, e.g. "fireworks"
|
|
700
762
|
label: z.string().min(1).optional(),
|
|
701
763
|
api: ModelProviderApi.default("chat"),
|
|
702
764
|
baseUrl: z.string().url(),
|
|
703
|
-
apiKey: z.string().optional(),
|
|
704
|
-
apiKeyEnv: z.string().optional(),
|
|
765
|
+
apiKey: z.string().optional(), // inline key (pragmatic) ...
|
|
766
|
+
apiKeyEnv: z.string().optional(), // ... OR name of the env var holding the key (preferred)
|
|
705
767
|
defaultQuery: z.record(z.string(), z.string()).optional(),
|
|
706
768
|
defaultHeaders: z.record(z.string(), z.string()).optional(),
|
|
707
769
|
models: z.array(RegistryModelSchema).min(1),
|
|
@@ -711,28 +773,29 @@ export type RegistryProvider = z.infer<typeof RegistryProviderSchema>;
|
|
|
711
773
|
export const IntegrationOAuthClientConfigSchema = z.object({
|
|
712
774
|
clientId: z.string().min(1),
|
|
713
775
|
clientSecret: z.string().min(1).optional(),
|
|
714
|
-
tokenEndpointAuthMethod: z
|
|
776
|
+
tokenEndpointAuthMethod: z
|
|
777
|
+
.enum(["none", "client_secret_post", "client_secret_basic"])
|
|
778
|
+
.default("none"),
|
|
715
779
|
});
|
|
716
780
|
export type IntegrationOAuthClientConfig = z.infer<typeof IntegrationOAuthClientConfigSchema>;
|
|
717
781
|
|
|
718
782
|
/**
|
|
719
783
|
* Runtime-resolved provider (built-in or registry), client-construction-ready.
|
|
720
784
|
* The built-in OpenAI/Azure provider is always present and always "responses";
|
|
721
|
-
* registry providers carry their own base URL / key / wire API.
|
|
722
|
-
*
|
|
723
|
-
*
|
|
785
|
+
* registry providers carry their own base URL / key / wire API. Compaction is
|
|
786
|
+
* not a provider capability: all providers use the same durable plaintext
|
|
787
|
+
* replacement.
|
|
724
788
|
*/
|
|
725
789
|
export interface ResolvedModelProvider {
|
|
726
|
-
id: string;
|
|
790
|
+
id: string; // "openai" | "azure" | registry id
|
|
727
791
|
label: string;
|
|
728
|
-
kind: RegistryProviderKind;
|
|
792
|
+
kind: RegistryProviderKind; // "api-key" (built-ins + most registry) | "codex-subscription"
|
|
729
793
|
api: ModelProviderApi;
|
|
730
794
|
builtin: boolean;
|
|
731
795
|
baseUrl?: string | undefined;
|
|
732
796
|
apiKey?: string | undefined;
|
|
733
797
|
defaultQuery?: Record<string, string> | undefined;
|
|
734
798
|
defaultHeaders?: Record<string, string> | undefined;
|
|
735
|
-
compactionMode: ContextCompactionMode; // "server" only for built-in OpenAI; "client" otherwise
|
|
736
799
|
}
|
|
737
800
|
|
|
738
801
|
/** A single exposed model + the provider that serves it. */
|
|
@@ -743,17 +806,31 @@ export interface ConfiguredModel {
|
|
|
743
806
|
providerLabel: string;
|
|
744
807
|
api: ModelProviderApi;
|
|
745
808
|
contextWindowTokens?: number | undefined;
|
|
809
|
+
effectiveContextWindowTokens?: number | undefined;
|
|
810
|
+
autoCompactTokenLimit?: number | undefined;
|
|
746
811
|
reasoningEffort: boolean;
|
|
747
812
|
hostedWebSearch: boolean;
|
|
748
813
|
}
|
|
749
814
|
|
|
750
815
|
export const defaultModelPricing: Record<string, ModelPricing> = {
|
|
751
|
-
"gpt-5.
|
|
816
|
+
"gpt-5.6-sol": {
|
|
752
817
|
inputMicrosPerMillionTokens: 5_000_000,
|
|
753
818
|
cachedInputMicrosPerMillionTokens: 500_000,
|
|
754
819
|
outputMicrosPerMillionTokens: 30_000_000,
|
|
755
820
|
marginBps: 2_500,
|
|
756
821
|
},
|
|
822
|
+
"gpt-5.6-terra": {
|
|
823
|
+
inputMicrosPerMillionTokens: 2_500_000,
|
|
824
|
+
cachedInputMicrosPerMillionTokens: 250_000,
|
|
825
|
+
outputMicrosPerMillionTokens: 15_000_000,
|
|
826
|
+
marginBps: 2_500,
|
|
827
|
+
},
|
|
828
|
+
"gpt-5.6-luna": {
|
|
829
|
+
inputMicrosPerMillionTokens: 1_000_000,
|
|
830
|
+
cachedInputMicrosPerMillionTokens: 100_000,
|
|
831
|
+
outputMicrosPerMillionTokens: 6_000_000,
|
|
832
|
+
marginBps: 2_500,
|
|
833
|
+
},
|
|
757
834
|
"gpt-5.4": {
|
|
758
835
|
inputMicrosPerMillionTokens: 2_500_000,
|
|
759
836
|
cachedInputMicrosPerMillionTokens: 250_000,
|
|
@@ -839,7 +916,10 @@ export type SandboxRequiredEnv = {
|
|
|
839
916
|
env: string;
|
|
840
917
|
};
|
|
841
918
|
|
|
842
|
-
export const SANDBOX_REQUIRED_ENV: Record<
|
|
919
|
+
export const SANDBOX_REQUIRED_ENV: Record<
|
|
920
|
+
z.infer<typeof SandboxBackend>,
|
|
921
|
+
readonly SandboxRequiredEnv[]
|
|
922
|
+
> = {
|
|
843
923
|
// docker/local/none need no credentials (local dev container / in-process / off).
|
|
844
924
|
docker: [],
|
|
845
925
|
local: [],
|
|
@@ -849,21 +929,11 @@ export const SANDBOX_REQUIRED_ENV: Record<z.infer<typeof SandboxBackend>, readon
|
|
|
849
929
|
{ field: "modalTokenId", env: "OPENGENI_MODAL_TOKEN_ID" },
|
|
850
930
|
{ field: "modalTokenSecret", env: "OPENGENI_MODAL_TOKEN_SECRET" },
|
|
851
931
|
],
|
|
852
|
-
daytona: [
|
|
853
|
-
|
|
854
|
-
],
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
],
|
|
858
|
-
e2b: [
|
|
859
|
-
{ field: "e2bApiKey", env: "OPENGENI_E2B_API_KEY" },
|
|
860
|
-
],
|
|
861
|
-
blaxel: [
|
|
862
|
-
{ field: "blaxelApiKey", env: "OPENGENI_BLAXEL_API_KEY" },
|
|
863
|
-
],
|
|
864
|
-
cloudflare: [
|
|
865
|
-
{ field: "cloudflareWorkerUrl", env: "OPENGENI_CLOUDFLARE_WORKER_URL" },
|
|
866
|
-
],
|
|
932
|
+
daytona: [{ field: "daytonaApiKey", env: "OPENGENI_DAYTONA_API_KEY" }],
|
|
933
|
+
runloop: [{ field: "runloopApiKey", env: "OPENGENI_RUNLOOP_API_KEY" }],
|
|
934
|
+
e2b: [{ field: "e2bApiKey", env: "OPENGENI_E2B_API_KEY" }],
|
|
935
|
+
blaxel: [{ field: "blaxelApiKey", env: "OPENGENI_BLAXEL_API_KEY" }],
|
|
936
|
+
cloudflare: [{ field: "cloudflareWorkerUrl", env: "OPENGENI_CLOUDFLARE_WORKER_URL" }],
|
|
867
937
|
vercel: [
|
|
868
938
|
{ field: "vercelToken", env: "OPENGENI_VERCEL_TOKEN" },
|
|
869
939
|
{ field: "vercelProjectId", env: "OPENGENI_VERCEL_PROJECT_ID" },
|
|
@@ -889,7 +959,10 @@ export function getSettings(): Settings {
|
|
|
889
959
|
const raw = {
|
|
890
960
|
serviceName: optional("OPENGENI_SERVICE_NAME"),
|
|
891
961
|
environment: optional("OPENGENI_ENVIRONMENT"),
|
|
892
|
-
deploymentRevision:
|
|
962
|
+
deploymentRevision:
|
|
963
|
+
optional("OPENGENI_DEPLOYMENT_REVISION") ??
|
|
964
|
+
optional("SOURCE_VERSION") ??
|
|
965
|
+
optional("GITHUB_SHA"),
|
|
893
966
|
serverVersion: optional("OPENGENI_SERVER_VERSION"),
|
|
894
967
|
databaseUrl: optional("OPENGENI_DATABASE_URL"),
|
|
895
968
|
dbSchema: optional("OPENGENI_DB_SCHEMA"),
|
|
@@ -899,12 +972,16 @@ export function getSettings(): Settings {
|
|
|
899
972
|
temporalNamespace: optional("OPENGENI_TEMPORAL_NAMESPACE"),
|
|
900
973
|
temporalTaskQueue: optional("OPENGENI_TEMPORAL_TASK_QUEUE"),
|
|
901
974
|
startupDependencyRetryAttempts: optional("OPENGENI_STARTUP_DEPENDENCY_RETRY_ATTEMPTS"),
|
|
902
|
-
startupDependencyRetryInitialDelayMs: optional(
|
|
975
|
+
startupDependencyRetryInitialDelayMs: optional(
|
|
976
|
+
"OPENGENI_STARTUP_DEPENDENCY_RETRY_INITIAL_DELAY_MS",
|
|
977
|
+
),
|
|
903
978
|
startupDependencyRetryMaxDelayMs: optional("OPENGENI_STARTUP_DEPENDENCY_RETRY_MAX_DELAY_MS"),
|
|
904
979
|
observabilityStructuredLogs: optional("OPENGENI_OBSERVABILITY_STRUCTURED_LOGS"),
|
|
905
980
|
observabilityMetricsEnabled: optional("OPENGENI_OBSERVABILITY_METRICS_ENABLED"),
|
|
906
|
-
observabilityOtlpEndpoint:
|
|
907
|
-
|
|
981
|
+
observabilityOtlpEndpoint:
|
|
982
|
+
optional("OPENGENI_OTEL_EXPORTER_OTLP_ENDPOINT") ?? optional("OTEL_EXPORTER_OTLP_ENDPOINT"),
|
|
983
|
+
observabilityOtlpHeaders:
|
|
984
|
+
optional("OPENGENI_OTEL_EXPORTER_OTLP_HEADERS") ?? optional("OTEL_EXPORTER_OTLP_HEADERS"),
|
|
908
985
|
publicBaseUrl: optional("OPENGENI_PUBLIC_BASE_URL"),
|
|
909
986
|
agentReleasesBaseUrl: optional("OPENGENI_AGENT_RELEASES_BASE_URL"),
|
|
910
987
|
productAccessMode: optional("OPENGENI_PRODUCT_ACCESS_MODE"),
|
|
@@ -921,21 +998,18 @@ export function getSettings(): Settings {
|
|
|
921
998
|
environmentsEncryptionKey: optional("OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY"),
|
|
922
999
|
integrationsEnabled: optional("OPENGENI_INTEGRATIONS_ENABLED"),
|
|
923
1000
|
integrationsStateSecret: optional("OPENGENI_INTEGRATIONS_STATE_SECRET"),
|
|
924
|
-
integrationsAllowPrivateNetworkTargets: optional(
|
|
1001
|
+
integrationsAllowPrivateNetworkTargets: optional(
|
|
1002
|
+
"OPENGENI_INTEGRATIONS_ALLOW_PRIVATE_NETWORK_TARGETS",
|
|
1003
|
+
),
|
|
925
1004
|
integrationsOauthClientsJson: optional("OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON"),
|
|
926
1005
|
goalMaxAutoContinuations: optional("OPENGENI_GOAL_MAX_AUTO_CONTINUATIONS"),
|
|
927
1006
|
goalNoProgressLimit: optional("OPENGENI_GOAL_NO_PROGRESS_LIMIT"),
|
|
928
1007
|
agentMaxModelCallsPerTurn: optional("OPENGENI_AGENT_MAX_MODEL_CALLS_PER_TURN"),
|
|
929
|
-
sessionHistorySource: optional("OPENGENI_SESSION_HISTORY_SOURCE"),
|
|
930
|
-
contextCompactionMode: optional("OPENGENI_CONTEXT_COMPACTION_MODE"),
|
|
931
1008
|
contextWindowTokens: optional("OPENGENI_CONTEXT_WINDOW_TOKENS"),
|
|
1009
|
+
contextEffectiveWindowTokens: optional("OPENGENI_CONTEXT_EFFECTIVE_WINDOW_TOKENS"),
|
|
932
1010
|
contextCompactionThresholdRatio: optional("OPENGENI_COMPACTION_THRESHOLD_RATIO"),
|
|
933
1011
|
contextReservedOutputTokens: optional("OPENGENI_CONTEXT_RESERVED_OUTPUT_TOKENS"),
|
|
934
|
-
|
|
935
|
-
contextCompactSoftFraction: optional("OPENGENI_CONTEXT_COMPACT_SOFT_FRACTION"),
|
|
936
|
-
contextCompactHardFraction: optional("OPENGENI_CONTEXT_COMPACT_HARD_FRACTION"),
|
|
937
|
-
contextKeepRecentTokens: optional("OPENGENI_CONTEXT_KEEP_RECENT_TOKENS"),
|
|
938
|
-
contextSummaryMaxTokens: optional("OPENGENI_CONTEXT_SUMMARY_MAX_TOKENS"),
|
|
1012
|
+
contextAutoCompactThresholdTokens: optional("OPENGENI_CONTEXT_AUTO_COMPACT_THRESHOLD_TOKENS"),
|
|
939
1013
|
authRequired: optional("OPENGENI_AUTH_REQUIRED"),
|
|
940
1014
|
accessKey: optional("OPENGENI_ACCESS_KEY"),
|
|
941
1015
|
authAllowHealth: optional("OPENGENI_AUTH_ALLOW_HEALTH"),
|
|
@@ -954,6 +1028,7 @@ export function getSettings(): Settings {
|
|
|
954
1028
|
modelProvidersJson: optional("OPENGENI_MODEL_PROVIDERS_JSON"),
|
|
955
1029
|
codexSubscriptionEnabled: optional("OPENGENI_CODEX_SUBSCRIPTION_ENABLED"),
|
|
956
1030
|
codexToolSearchEnabled: optional("OPENGENI_CODEX_TOOL_SEARCH_ENABLED"),
|
|
1031
|
+
codexCredentialLeasingEnabled: optional("OPENGENI_CODEX_CREDENTIAL_LEASING_ENABLED"),
|
|
957
1032
|
codexProductSku: optional("OPENGENI_CODEX_PRODUCT_SKU"),
|
|
958
1033
|
codexRotationNearExhaustionPct: optional("OPENGENI_CODEX_ROTATION_NEAR_EXHAUSTION_PCT"),
|
|
959
1034
|
openaiReasoningEffort: optional("OPENGENI_OPENAI_REASONING_EFFORT"),
|
|
@@ -993,6 +1068,7 @@ export function getSettings(): Settings {
|
|
|
993
1068
|
computerUseEnabled: optional("OPENGENI_COMPUTER_USE_ENABLED"),
|
|
994
1069
|
computerUseReadOnly: optional("OPENGENI_COMPUTER_USE_READONLY"),
|
|
995
1070
|
recordingEnabled: optional("OPENGENI_RECORDING_ENABLED"),
|
|
1071
|
+
workspaceCaptureEnabled: optional("OPENGENI_WORKSPACE_CAPTURE"),
|
|
996
1072
|
recordingDefaultCodec: optional("OPENGENI_RECORDING_DEFAULT_CODEC"),
|
|
997
1073
|
recordingFramerate: optional("OPENGENI_RECORDING_FRAMERATE"),
|
|
998
1074
|
recordingMaxSeconds: optional("OPENGENI_RECORDING_MAX_SECONDS"),
|
|
@@ -1032,7 +1108,9 @@ export function getSettings(): Settings {
|
|
|
1032
1108
|
vercelTeamId: optional("OPENGENI_VERCEL_TEAM_ID"),
|
|
1033
1109
|
vercelRuntime: optional("OPENGENI_VERCEL_RUNTIME"),
|
|
1034
1110
|
sandboxOwnershipEnabled: optional("OPENGENI_SANDBOX_OWNERSHIP_ENABLED"),
|
|
1111
|
+
sandboxLazyProvisionEnabled: optional("OPENGENI_SANDBOX_LAZY_PROVISION"),
|
|
1035
1112
|
sandboxSelfhostedEnabled: optional("OPENGENI_SANDBOX_SELFHOSTED_ENABLED"),
|
|
1113
|
+
agentOpStreamEnabled: optional("OPENGENI_AGENT_OP_STREAM_ENABLED"),
|
|
1036
1114
|
enrollmentSigningSecret: optional("OPENGENI_ENROLLMENT_SIGNING_SECRET"),
|
|
1037
1115
|
selfhostedNatsUrl: optional("OPENGENI_SELFHOSTED_NATS_URL"),
|
|
1038
1116
|
selfhostedRelayUrl: optional("OPENGENI_SELFHOSTED_RELAY_URL"),
|
|
@@ -1044,13 +1122,20 @@ export function getSettings(): Settings {
|
|
|
1044
1122
|
selfhostedNatsCalloutPassword: optional("OPENGENI_SELFHOSTED_NATS_CALLOUT_PASSWORD"),
|
|
1045
1123
|
selfhostedNatsControlUser: optional("OPENGENI_SELFHOSTED_NATS_CONTROL_USER"),
|
|
1046
1124
|
selfhostedNatsControlPassword: optional("OPENGENI_SELFHOSTED_NATS_CONTROL_PASSWORD"),
|
|
1125
|
+
sandboxSelfhostedExecTimeoutMs: optional("OPENGENI_SANDBOX_SELFHOSTED_EXEC_TIMEOUT_MS"),
|
|
1126
|
+
sandboxSelfhostedControlTimeoutMs: optional("OPENGENI_SANDBOX_SELFHOSTED_CONTROL_TIMEOUT_MS"),
|
|
1047
1127
|
sandboxLeaseReaperPeriodMs: optional("OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS"),
|
|
1048
1128
|
sandboxViewerHolderTtlMs: optional("OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS"),
|
|
1049
1129
|
sandboxIdleGraceMs: optional("OPENGENI_SANDBOX_IDLE_GRACE_MS"),
|
|
1130
|
+
sandboxSnapshotIntervalMs: optional("OPENGENI_SANDBOX_SNAPSHOT_INTERVAL_MS"),
|
|
1131
|
+
sandboxSnapshotTimeoutMs: optional("OPENGENI_SANDBOX_SNAPSHOT_TIMEOUT_MS"),
|
|
1050
1132
|
sandboxLeaseTtlMs: optional("OPENGENI_SANDBOX_LEASE_TTL_MS"),
|
|
1051
1133
|
sandboxLeaseWarmingTtlMs: optional("OPENGENI_SANDBOX_LEASE_WARMING_TTL_MS"),
|
|
1052
1134
|
sandboxWarmingTimeoutMs: optional("OPENGENI_SANDBOX_WARMING_TIMEOUT_MS"),
|
|
1053
|
-
|
|
1135
|
+
rigSetupTimeoutMs: optional("OPENGENI_RIG_SETUP_TIMEOUT_MS"),
|
|
1136
|
+
sandboxWarmRateMicrosPerSecondJson: optional(
|
|
1137
|
+
"OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON",
|
|
1138
|
+
),
|
|
1054
1139
|
sandboxMaxWarmSecondsPerWorkspace: optional("OPENGENI_SANDBOX_MAX_WARM_SECONDS_PER_WORKSPACE"),
|
|
1055
1140
|
sandboxPreparationProfiles: optional("OPENGENI_SANDBOX_PREPARATION_PROFILES"),
|
|
1056
1141
|
sandboxEnvAllowlist: optional("OPENGENI_SANDBOX_ENV_ALLOWLIST"),
|
|
@@ -1127,7 +1212,10 @@ export function effectiveModalIdleTimeoutSeconds(settings: Settings): number {
|
|
|
1127
1212
|
return settings.modalIdleTimeoutSeconds ?? settings.modalTimeoutSeconds;
|
|
1128
1213
|
}
|
|
1129
1214
|
|
|
1130
|
-
export function collectSandboxEnvironment(
|
|
1215
|
+
export function collectSandboxEnvironment(
|
|
1216
|
+
settings: Settings,
|
|
1217
|
+
source: NodeJS.ProcessEnv = process.env,
|
|
1218
|
+
): Record<string, string> {
|
|
1131
1219
|
const out: Record<string, string> = {};
|
|
1132
1220
|
for (const name of sandboxEnvironmentVariableNames(settings)) {
|
|
1133
1221
|
const value = source[name];
|
|
@@ -1159,8 +1247,14 @@ export function resolveProviderApiKey(
|
|
|
1159
1247
|
return undefined;
|
|
1160
1248
|
}
|
|
1161
1249
|
|
|
1162
|
-
/**
|
|
1163
|
-
|
|
1250
|
+
/**
|
|
1251
|
+
* The built-in provider's stable id: "openai" on the OpenAI platform, "azure"
|
|
1252
|
+
* on Azure. Exported because the workspace model-policy gate must attribute
|
|
1253
|
+
* the legacy resolveTurnModel-null fallback (which routes to this built-in
|
|
1254
|
+
* client) to the SAME identity the router uses — otherwise a policy blocking
|
|
1255
|
+
* the built-in could be bypassed through the null-resolution path.
|
|
1256
|
+
*/
|
|
1257
|
+
export function builtinProviderId(settings: Pick<Settings, "openaiProvider">): string {
|
|
1164
1258
|
return settings.openaiProvider === "azure" ? "azure" : "openai";
|
|
1165
1259
|
}
|
|
1166
1260
|
|
|
@@ -1170,9 +1264,8 @@ function builtinProviderLabel(settings: Pick<Settings, "openaiProvider">): strin
|
|
|
1170
1264
|
|
|
1171
1265
|
/**
|
|
1172
1266
|
* Every provider a client may route to: the built-in OpenAI/Azure provider
|
|
1173
|
-
* first (id "openai"/"azure", always "responses",
|
|
1174
|
-
*
|
|
1175
|
-
* order (compactionMode "client"). Client-construction inputs are filled from
|
|
1267
|
+
* first (id "openai"/"azure", always "responses"), then each registry provider
|
|
1268
|
+
* in declaration order. Client-construction inputs are filled from
|
|
1176
1269
|
* the existing flat openai/azure settings for the built-in, and from the
|
|
1177
1270
|
* registry entry for the rest. Registry ids may not collide with the built-in
|
|
1178
1271
|
* id — validateSettings rejects that at boot.
|
|
@@ -1184,7 +1277,6 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
|
|
|
1184
1277
|
kind: "api-key",
|
|
1185
1278
|
api: "responses",
|
|
1186
1279
|
builtin: true,
|
|
1187
|
-
compactionMode: resolveContextCompactionMode(settings),
|
|
1188
1280
|
};
|
|
1189
1281
|
if (settings.openaiProvider === "azure") {
|
|
1190
1282
|
builtin.baseUrl = settings.azureOpenaiBaseUrl ?? settings.azureOpenaiEndpoint;
|
|
@@ -1193,21 +1285,43 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
|
|
|
1193
1285
|
builtin.baseUrl = settings.openaiBaseUrl;
|
|
1194
1286
|
builtin.apiKey = settings.openaiApiKey;
|
|
1195
1287
|
}
|
|
1196
|
-
const registry = parseModelProvidersJson(settings.modelProvidersJson).map(
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1288
|
+
const registry = parseModelProvidersJson(settings.modelProvidersJson).map(
|
|
1289
|
+
(provider): ResolvedModelProvider => ({
|
|
1290
|
+
id: provider.id,
|
|
1291
|
+
label: provider.label ?? provider.id,
|
|
1292
|
+
kind: provider.kind,
|
|
1293
|
+
api: provider.api,
|
|
1294
|
+
builtin: false,
|
|
1295
|
+
baseUrl: provider.baseUrl,
|
|
1296
|
+
apiKey: resolveProviderApiKey(provider),
|
|
1297
|
+
defaultQuery: provider.defaultQuery,
|
|
1298
|
+
defaultHeaders: provider.defaultHeaders,
|
|
1299
|
+
}),
|
|
1300
|
+
);
|
|
1208
1301
|
return [builtin, ...registry];
|
|
1209
1302
|
}
|
|
1210
1303
|
|
|
1304
|
+
/**
|
|
1305
|
+
* The provider identity a model id resolves to, for workspace model-policy
|
|
1306
|
+
* evaluation — MUST agree with the real router (resolveTurnModel /
|
|
1307
|
+
* MultiProviderModelProvider) on every case:
|
|
1308
|
+
* - `codex/<slug>` → the codex-subscription provider id, ALWAYS. With no
|
|
1309
|
+
* active subscription the router fails loud (CodexSubscriptionUnavailableError),
|
|
1310
|
+
* never the built-in — so attributing by prefix is exact even against BASE
|
|
1311
|
+
* settings where the overlay provider is not injected.
|
|
1312
|
+
* - a configured model id → its configuredModels providerId (registry or built-in).
|
|
1313
|
+
* - anything else → the built-in id: an unknown id is the legacy
|
|
1314
|
+
* resolveTurnModel-null fallback, which the built-in OpenAI/Azure client
|
|
1315
|
+
* serves. A policy blocking the built-in must block this path too.
|
|
1316
|
+
*/
|
|
1317
|
+
export function policyProviderIdForModel(settings: Settings, modelId: string): string {
|
|
1318
|
+
if (modelId.startsWith(CODEX_MODEL_ID_PREFIX)) {
|
|
1319
|
+
return CODEX_PROVIDER_ID;
|
|
1320
|
+
}
|
|
1321
|
+
const configured = configuredModels(settings).find((model) => model.id === modelId);
|
|
1322
|
+
return configured?.providerId ?? builtinProviderId(settings);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1211
1325
|
/**
|
|
1212
1326
|
* Every model a client may use, the built-in provider's models first
|
|
1213
1327
|
* (configuredAllowedModels-from-openai, mapped to "responses" with
|
|
@@ -1230,17 +1344,22 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
|
|
|
1230
1344
|
// contains "/") that a registry actually owns is never a valid Azure/OpenAI
|
|
1231
1345
|
// deployment name, and a `codex/`-prefixed id never is either — exclude both
|
|
1232
1346
|
// from the built-in list. A BARE id a registry merely redeclares (e.g.
|
|
1233
|
-
// "gpt-5.
|
|
1347
|
+
// "gpt-5.6-sol") is left in place so the built-in still wins it via the first-wins
|
|
1234
1348
|
// de-dup below (preserving the documented built-in-precedence contract). When
|
|
1235
1349
|
// a codex/ id has NO codex provider injected (no active subscription) it then
|
|
1236
1350
|
// resolves to nothing and getModel fails loud with
|
|
1237
1351
|
// CodexSubscriptionUnavailableError instead of mis-routing to Azure.
|
|
1238
1352
|
const registryOwnedIds = new Set(
|
|
1239
|
-
parseModelProvidersJson(settings.modelProvidersJson).flatMap((provider) =>
|
|
1353
|
+
parseModelProvidersJson(settings.modelProvidersJson).flatMap((provider) =>
|
|
1354
|
+
provider.models.map((model) => model.id),
|
|
1355
|
+
),
|
|
1240
1356
|
);
|
|
1241
1357
|
const isRegistryNamespaced = (id: string): boolean =>
|
|
1242
1358
|
id.startsWith(CODEX_MODEL_ID_PREFIX) || (id.includes("/") && registryOwnedIds.has(id));
|
|
1243
|
-
const out: ConfiguredModel[] = uniqueValues([
|
|
1359
|
+
const out: ConfiguredModel[] = uniqueValues([
|
|
1360
|
+
settings.openaiModel,
|
|
1361
|
+
...splitCsv(settings.openaiAllowedModels),
|
|
1362
|
+
])
|
|
1244
1363
|
.filter((id) => !isRegistryNamespaced(id))
|
|
1245
1364
|
.map((id) => ({
|
|
1246
1365
|
id,
|
|
@@ -1261,7 +1380,15 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
|
|
|
1261
1380
|
providerId: provider.id,
|
|
1262
1381
|
providerLabel,
|
|
1263
1382
|
api: provider.api,
|
|
1264
|
-
...(model.contextWindowTokens === undefined
|
|
1383
|
+
...(model.contextWindowTokens === undefined
|
|
1384
|
+
? {}
|
|
1385
|
+
: { contextWindowTokens: model.contextWindowTokens }),
|
|
1386
|
+
...(model.effectiveContextWindowTokens === undefined
|
|
1387
|
+
? {}
|
|
1388
|
+
: { effectiveContextWindowTokens: model.effectiveContextWindowTokens }),
|
|
1389
|
+
...(model.autoCompactTokenLimit === undefined
|
|
1390
|
+
? {}
|
|
1391
|
+
: { autoCompactTokenLimit: model.autoCompactTokenLimit }),
|
|
1265
1392
|
reasoningEffort: model.reasoningEffort ?? false,
|
|
1266
1393
|
hostedWebSearch: model.hostedWebSearch ?? false,
|
|
1267
1394
|
});
|
|
@@ -1302,7 +1429,9 @@ export function resolveModelProvider(
|
|
|
1302
1429
|
if (!model) {
|
|
1303
1430
|
return undefined;
|
|
1304
1431
|
}
|
|
1305
|
-
const provider = configuredProviders(settings).find(
|
|
1432
|
+
const provider = configuredProviders(settings).find(
|
|
1433
|
+
(candidate) => candidate.id === model.providerId,
|
|
1434
|
+
);
|
|
1306
1435
|
if (!provider) {
|
|
1307
1436
|
return undefined;
|
|
1308
1437
|
}
|
|
@@ -1332,49 +1461,49 @@ export function configuredModelPricing(settings: Settings): Record<string, Model
|
|
|
1332
1461
|
}
|
|
1333
1462
|
|
|
1334
1463
|
/**
|
|
1335
|
-
*
|
|
1336
|
-
*
|
|
1337
|
-
* SDK emits context_management; we pass the correct gpt-5.5 threshold).
|
|
1338
|
-
* - "client": run OpenGeni's own client-side compaction (Azure and any other
|
|
1339
|
-
* backend that rejects/ignores context_management).
|
|
1340
|
-
* - "off": neither (legacy unbounded growth; escape hatch).
|
|
1341
|
-
*
|
|
1342
|
-
* "auto" maps to "server" on the OpenAI platform provider and "client"
|
|
1343
|
-
* otherwise — Azure's Responses API returns 400 unsupported_parameter for
|
|
1344
|
-
* context_management, so it must never take the server path.
|
|
1464
|
+
* Usable input-token budget: an explicit model-catalog effective window when
|
|
1465
|
+
* available, otherwise the deployment window minus its output reserve.
|
|
1345
1466
|
*/
|
|
1346
|
-
export
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
case "off":
|
|
1355
|
-
return "off";
|
|
1356
|
-
case "auto":
|
|
1357
|
-
default:
|
|
1358
|
-
return settings.openaiProvider === "openai" ? "server" : "client";
|
|
1467
|
+
export function contextInputBudgetTokens(
|
|
1468
|
+
settings: Pick<
|
|
1469
|
+
Settings,
|
|
1470
|
+
"contextWindowTokens" | "contextEffectiveWindowTokens" | "contextReservedOutputTokens"
|
|
1471
|
+
>,
|
|
1472
|
+
): number {
|
|
1473
|
+
if (settings.contextEffectiveWindowTokens !== undefined) {
|
|
1474
|
+
return Math.min(settings.contextWindowTokens, settings.contextEffectiveWindowTokens);
|
|
1359
1475
|
}
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
/** Usable input-token budget B = window - reserved output. */
|
|
1363
|
-
export function contextInputBudgetTokens(settings: Pick<Settings, "contextWindowTokens" | "contextReservedOutputTokens">): number {
|
|
1364
1476
|
return Math.max(0, settings.contextWindowTokens - settings.contextReservedOutputTokens);
|
|
1365
1477
|
}
|
|
1366
1478
|
|
|
1367
1479
|
/**
|
|
1368
|
-
*
|
|
1369
|
-
*
|
|
1370
|
-
*
|
|
1371
|
-
* fallback for gpt-5.5 (which is absent from its hardcoded window map).
|
|
1480
|
+
* Apply the resolved provider/model's context policy to one turn. Registry
|
|
1481
|
+
* metadata is authoritative when present; deployment defaults remain the
|
|
1482
|
+
* fallback for models that do not declare their own limits.
|
|
1372
1483
|
*/
|
|
1373
|
-
export function
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1484
|
+
export function settingsWithResolvedModelContext(
|
|
1485
|
+
settings: Settings,
|
|
1486
|
+
model: Pick<
|
|
1487
|
+
ConfiguredModel,
|
|
1488
|
+
"contextWindowTokens" | "effectiveContextWindowTokens" | "autoCompactTokenLimit"
|
|
1489
|
+
>,
|
|
1490
|
+
): Settings {
|
|
1491
|
+
const contextWindowTokens = model.contextWindowTokens ?? settings.contextWindowTokens;
|
|
1492
|
+
return {
|
|
1493
|
+
...settings,
|
|
1494
|
+
contextWindowTokens,
|
|
1495
|
+
...(model.effectiveContextWindowTokens === undefined
|
|
1496
|
+
? {}
|
|
1497
|
+
: {
|
|
1498
|
+
contextEffectiveWindowTokens: Math.min(
|
|
1499
|
+
contextWindowTokens,
|
|
1500
|
+
model.effectiveContextWindowTokens,
|
|
1501
|
+
),
|
|
1502
|
+
}),
|
|
1503
|
+
...(model.autoCompactTokenLimit === undefined
|
|
1504
|
+
? {}
|
|
1505
|
+
: { contextAutoCompactThresholdTokens: model.autoCompactTokenLimit }),
|
|
1506
|
+
};
|
|
1378
1507
|
}
|
|
1379
1508
|
|
|
1380
1509
|
export function configuredStaticUsageLimits(settings: Settings): StaticUsageLimitsConfig {
|
|
@@ -1399,20 +1528,31 @@ export function configuredEntitlements(settings: Settings): EntitlementsConfig {
|
|
|
1399
1528
|
};
|
|
1400
1529
|
}
|
|
1401
1530
|
|
|
1402
|
-
export function calculateModelUsageCostMicros(
|
|
1531
|
+
export function calculateModelUsageCostMicros(
|
|
1532
|
+
settings: Settings,
|
|
1533
|
+
model: string,
|
|
1534
|
+
usage: ModelUsageInput,
|
|
1535
|
+
): number {
|
|
1403
1536
|
const pricing = configuredModelPricing(settings)[model];
|
|
1404
1537
|
if (!pricing) {
|
|
1405
1538
|
throw new Error(`Missing model pricing for ${model}`);
|
|
1406
1539
|
}
|
|
1407
|
-
const entries =
|
|
1540
|
+
const entries =
|
|
1541
|
+
usage.requestUsageEntries && usage.requestUsageEntries.length > 0
|
|
1542
|
+
? usage.requestUsageEntries
|
|
1543
|
+
: [usage];
|
|
1408
1544
|
const rawCost = entries.reduce((sum, entry) => sum + calculateEntryCostMicros(pricing, entry), 0);
|
|
1409
1545
|
const marginBps = pricing.marginBps ?? 0;
|
|
1410
|
-
return Math.ceil(rawCost * (10_000 + marginBps) / 10_000);
|
|
1546
|
+
return Math.ceil((rawCost * (10_000 + marginBps)) / 10_000);
|
|
1411
1547
|
}
|
|
1412
1548
|
|
|
1413
|
-
export function configuredAllowedReasoningEfforts(
|
|
1414
|
-
|
|
1415
|
-
|
|
1549
|
+
export function configuredAllowedReasoningEfforts(
|
|
1550
|
+
settings: Settings,
|
|
1551
|
+
): Array<z.infer<typeof ReasoningEffort>> {
|
|
1552
|
+
return uniqueValues([
|
|
1553
|
+
settings.openaiReasoningEffort,
|
|
1554
|
+
...splitCsv(settings.openaiAllowedReasoningEfforts),
|
|
1555
|
+
]).map((value) => ReasoningEffort.parse(value));
|
|
1416
1556
|
}
|
|
1417
1557
|
|
|
1418
1558
|
/**
|
|
@@ -1426,7 +1566,9 @@ export function environmentsEncryptionKeyBytes(settings: Settings): Uint8Array |
|
|
|
1426
1566
|
}
|
|
1427
1567
|
const decoded = Buffer.from(settings.environmentsEncryptionKey, "base64");
|
|
1428
1568
|
if (decoded.length !== 32) {
|
|
1429
|
-
throw new Error(
|
|
1569
|
+
throw new Error(
|
|
1570
|
+
"OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY must be base64 for exactly 32 bytes (generate with: openssl rand -base64 32)",
|
|
1571
|
+
);
|
|
1430
1572
|
}
|
|
1431
1573
|
return new Uint8Array(decoded);
|
|
1432
1574
|
}
|
|
@@ -1453,12 +1595,24 @@ export function dbSearchPath(settings: Pick<Settings, "dbSchema">): string | und
|
|
|
1453
1595
|
}
|
|
1454
1596
|
|
|
1455
1597
|
export function collectGitIdentityEnvironment(settings: Settings): Record<string, string> {
|
|
1456
|
-
return Object.fromEntries(
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1598
|
+
return Object.fromEntries(
|
|
1599
|
+
Object.entries({
|
|
1600
|
+
GIT_AUTHOR_NAME: settings.gitAuthorName,
|
|
1601
|
+
GIT_AUTHOR_EMAIL: settings.gitAuthorEmail,
|
|
1602
|
+
GIT_COMMITTER_NAME: settings.gitCommitterName ?? settings.gitAuthorName,
|
|
1603
|
+
GIT_COMMITTER_EMAIL: settings.gitCommitterEmail ?? settings.gitAuthorEmail,
|
|
1604
|
+
}).filter(
|
|
1605
|
+
(entry): entry is [string, string] =>
|
|
1606
|
+
typeof entry[1] === "string" && entry[1].trim().length > 0,
|
|
1607
|
+
),
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const DEFAULT_SANDBOX_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
1612
|
+
|
|
1613
|
+
function prependPathEntry(pathValue: string | undefined, entry: string): string {
|
|
1614
|
+
const parts = (pathValue ?? DEFAULT_SANDBOX_PATH).split(":").filter(Boolean);
|
|
1615
|
+
return [entry, ...parts.filter((part) => part !== entry)].join(":");
|
|
1462
1616
|
}
|
|
1463
1617
|
|
|
1464
1618
|
/**
|
|
@@ -1476,20 +1630,22 @@ export function collectGitIdentityEnvironment(settings: Settings): Record<string
|
|
|
1476
1630
|
* workspace environment < the backend-aware HOME default. Reserved-name validation
|
|
1477
1631
|
* at write time keeps workspace values from colliding with platform entries.
|
|
1478
1632
|
*
|
|
1479
|
-
* DELIBERATELY EXCLUDES the per-run, ROTATING
|
|
1480
|
-
*
|
|
1633
|
+
* DELIBERATELY EXCLUDES the per-run, ROTATING git provider token VALUES that
|
|
1634
|
+
* `sandboxEnvironmentForRun` mints when a repository resource is
|
|
1481
1635
|
* attached: that token is minted FRESH per call, so it is not a stable, attach-
|
|
1482
1636
|
* reproducible value and must not be part of the shared base. Under the token-
|
|
1483
|
-
* broker (B1)
|
|
1484
|
-
*
|
|
1485
|
-
*
|
|
1486
|
-
*
|
|
1487
|
-
*
|
|
1488
|
-
*
|
|
1489
|
-
*
|
|
1490
|
-
*
|
|
1491
|
-
*
|
|
1492
|
-
*
|
|
1637
|
+
* broker (B1) token VALUES never ride the manifest at all — they are seeded to
|
|
1638
|
+
* FILES inside the box and git/provider CLI auth reads those files. What IS stable
|
|
1639
|
+
* and lives here for provisioned boxes are the token directory / GitHub alias
|
|
1640
|
+
* FILE PATH and wrapper PATH entries: constants derived from HOME, so they
|
|
1641
|
+
* appear IDENTICALLY on BOTH the turn AND every attach manifest (the SDK's
|
|
1642
|
+
* per-turn provided-session env delta stays empty even as tokens rotate). These
|
|
1643
|
+
* helper pointers are deliberately not added for selfhosted/local/none because
|
|
1644
|
+
* the platform never mints or seeds git provider tokens there. The attach
|
|
1645
|
+
* surfaces have only the `Session` (no repo resources) and so never seed a token,
|
|
1646
|
+
* but unwritten files simply yield no auth; the BLOCKING attach-vs-turn error
|
|
1647
|
+
* this helper fixes is for the common (no-repo) and workspace-environment-attached
|
|
1648
|
+
* provisioned-box cases.
|
|
1493
1649
|
*/
|
|
1494
1650
|
export function stableSandboxEnvironmentForRun(
|
|
1495
1651
|
settings: Settings,
|
|
@@ -1508,18 +1664,30 @@ export function stableSandboxEnvironmentForRun(
|
|
|
1508
1664
|
if (settings.sandboxBackend !== "none" && settings.sandboxBackend !== "local") {
|
|
1509
1665
|
environment.HOME ??= descriptor.workspaceRoot;
|
|
1510
1666
|
}
|
|
1511
|
-
// TOKEN-BROKER (B1): the STABLE
|
|
1512
|
-
//
|
|
1513
|
-
//
|
|
1514
|
-
//
|
|
1515
|
-
//
|
|
1516
|
-
//
|
|
1517
|
-
|
|
1518
|
-
|
|
1667
|
+
// TOKEN-BROKER (B1): the STABLE credential FILE PATHS and CLI wrapper PATH for
|
|
1668
|
+
// provisioned boxes only. Constants derived from the resolved HOME (falling
|
|
1669
|
+
// back to the descriptor workspaceRoot), so they are parity-safe — they join
|
|
1670
|
+
// the shared base and therefore appear IDENTICALLY on BOTH the worker-turn
|
|
1671
|
+
// manifest AND every API-direct attach manifest. Only PATHS are stable; token
|
|
1672
|
+
// VALUES live exclusively in files that runtime seeds off-manifest.
|
|
1673
|
+
const provisionedGitHelperBackend =
|
|
1674
|
+
settings.sandboxBackend !== "none" &&
|
|
1675
|
+
settings.sandboxBackend !== "local" &&
|
|
1676
|
+
settings.sandboxBackend !== "selfhosted";
|
|
1677
|
+
if (provisionedGitHelperBackend) {
|
|
1678
|
+
const home = environment.HOME ?? descriptor.workspaceRoot;
|
|
1679
|
+
environment.OPENGENI_GIT_CREDENTIALS_DIR ??= `${home}/.opengeni/git-credentials`;
|
|
1680
|
+
environment.OPENGENI_GIT_TOKEN_FILE ??= `${home}/.opengeni/git-token`;
|
|
1681
|
+
environment.OPENGENI_GIT_CLI_WRAPPER_DIR ??= `${home}/.opengeni/bin`;
|
|
1682
|
+
environment.PATH = prependPathEntry(environment.PATH, environment.OPENGENI_GIT_CLI_WRAPPER_DIR);
|
|
1683
|
+
}
|
|
1519
1684
|
if (settings.toolspaceEnabled) {
|
|
1520
1685
|
environment.OPENGENI_TOOLSPACE_TOKEN_FILE ??= `${environment.HOME ?? descriptor.workspaceRoot}/.opengeni/toolspace-token`;
|
|
1521
1686
|
if (options.workspaceId) {
|
|
1522
|
-
environment.OPENGENI_TOOLSPACE_URL ??= firstPartyMcpWorkspaceUrl(
|
|
1687
|
+
environment.OPENGENI_TOOLSPACE_URL ??= firstPartyMcpWorkspaceUrl(
|
|
1688
|
+
settings,
|
|
1689
|
+
options.workspaceId,
|
|
1690
|
+
);
|
|
1523
1691
|
}
|
|
1524
1692
|
}
|
|
1525
1693
|
return environment;
|
|
@@ -1532,11 +1700,45 @@ export function stableSandboxEnvironmentForRun(
|
|
|
1532
1700
|
* an attach-warmed cold box carries the IDENTICAL manifest env a later repo turn
|
|
1533
1701
|
* declares (env parity — see applyGitAuthPointerEnvironment).
|
|
1534
1702
|
*/
|
|
1535
|
-
export function hasGitHubRepositorySelection(
|
|
1703
|
+
export function hasGitHubRepositorySelection(
|
|
1704
|
+
resources: ReadonlyArray<{
|
|
1705
|
+
kind: string;
|
|
1706
|
+
provider?: unknown;
|
|
1707
|
+
installationId?: unknown;
|
|
1708
|
+
repositoryId?: unknown;
|
|
1709
|
+
githubInstallationId?: unknown;
|
|
1710
|
+
githubRepositoryId?: unknown;
|
|
1711
|
+
}>,
|
|
1712
|
+
): boolean {
|
|
1536
1713
|
const positive = (value: unknown): boolean =>
|
|
1537
|
-
(typeof value === "number" && Number.isInteger(value) && value > 0)
|
|
1538
|
-
|
|
1539
|
-
return resources.some(
|
|
1714
|
+
(typeof value === "number" && Number.isInteger(value) && value > 0) ||
|
|
1715
|
+
(typeof value === "string" && /^\d+$/.test(value) && Number(value) > 0);
|
|
1716
|
+
return resources.some(
|
|
1717
|
+
(resource) =>
|
|
1718
|
+
resource.kind === "repository" &&
|
|
1719
|
+
((positive(resource.githubInstallationId) && positive(resource.githubRepositoryId)) ||
|
|
1720
|
+
(resource.provider === "github" &&
|
|
1721
|
+
positive(resource.installationId) &&
|
|
1722
|
+
positive(resource.repositoryId))),
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
export function hasGitCredentialRepositorySelection(
|
|
1727
|
+
resources: ReadonlyArray<{
|
|
1728
|
+
kind: string;
|
|
1729
|
+
provider?: unknown;
|
|
1730
|
+
githubInstallationId?: unknown;
|
|
1731
|
+
githubRepositoryId?: unknown;
|
|
1732
|
+
}>,
|
|
1733
|
+
): boolean {
|
|
1734
|
+
return resources.some(
|
|
1735
|
+
(resource) =>
|
|
1736
|
+
resource.kind === "repository" &&
|
|
1737
|
+
(resource.provider === "github" ||
|
|
1738
|
+
resource.provider === "gitlab" ||
|
|
1739
|
+
resource.provider === "azure_devops" ||
|
|
1740
|
+
hasGitHubRepositorySelection([resource])),
|
|
1741
|
+
);
|
|
1540
1742
|
}
|
|
1541
1743
|
|
|
1542
1744
|
/**
|
|
@@ -1584,7 +1786,9 @@ export type StartupRetryOptions = {
|
|
|
1584
1786
|
}) => void;
|
|
1585
1787
|
};
|
|
1586
1788
|
|
|
1587
|
-
export function startupRetryOptions(
|
|
1789
|
+
export function startupRetryOptions(
|
|
1790
|
+
settings: Settings,
|
|
1791
|
+
): Required<Omit<StartupRetryOptions, "onRetry">> {
|
|
1588
1792
|
return {
|
|
1589
1793
|
attempts: settings.startupDependencyRetryAttempts,
|
|
1590
1794
|
initialDelayMs: settings.startupDependencyRetryInitialDelayMs,
|
|
@@ -1634,10 +1838,14 @@ export function sandboxLifecycleHookIds(settings: Settings): string[] {
|
|
|
1634
1838
|
}
|
|
1635
1839
|
|
|
1636
1840
|
function sandboxPreparationProfileNames(settings: Settings): string[] {
|
|
1637
|
-
const profiles = splitCsv(settings.sandboxPreparationProfiles).map((value) =>
|
|
1841
|
+
const profiles = splitCsv(settings.sandboxPreparationProfiles).map((value) =>
|
|
1842
|
+
value.toLowerCase(),
|
|
1843
|
+
);
|
|
1638
1844
|
if (profiles.includes("none")) {
|
|
1639
1845
|
if (profiles.length > 1) {
|
|
1640
|
-
throw new Error(
|
|
1846
|
+
throw new Error(
|
|
1847
|
+
"OPENGENI_SANDBOX_PREPARATION_PROFILES cannot combine none with other profiles",
|
|
1848
|
+
);
|
|
1641
1849
|
}
|
|
1642
1850
|
return ["none"];
|
|
1643
1851
|
}
|
|
@@ -1671,7 +1879,7 @@ export function parseMcpServers(raw: string | undefined): unknown[] | undefined
|
|
|
1671
1879
|
return parsed;
|
|
1672
1880
|
} catch (error) {
|
|
1673
1881
|
const message = error instanceof Error ? error.message : String(error);
|
|
1674
|
-
throw new Error(`OPENGENI_MCP_SERVERS must be a JSON array: ${message}
|
|
1882
|
+
throw new Error(`OPENGENI_MCP_SERVERS must be a JSON array: ${message}`, { cause: error });
|
|
1675
1883
|
}
|
|
1676
1884
|
}
|
|
1677
1885
|
|
|
@@ -1684,7 +1892,7 @@ export function parseModelPricingJson(raw: string): Record<string, ModelPricing>
|
|
|
1684
1892
|
parsed = JSON.parse(raw);
|
|
1685
1893
|
} catch (error) {
|
|
1686
1894
|
const message = error instanceof Error ? error.message : String(error);
|
|
1687
|
-
throw new Error(`OPENGENI_MODEL_PRICING_JSON must be valid JSON: ${message}
|
|
1895
|
+
throw new Error(`OPENGENI_MODEL_PRICING_JSON must be valid JSON: ${message}`, { cause: error });
|
|
1688
1896
|
}
|
|
1689
1897
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1690
1898
|
throw new Error("OPENGENI_MODEL_PRICING_JSON must be a JSON object keyed by model name");
|
|
@@ -1712,19 +1920,28 @@ export function parseSandboxWarmRateJson(raw: string): Record<string, number> {
|
|
|
1712
1920
|
parsed = JSON.parse(raw);
|
|
1713
1921
|
} catch (error) {
|
|
1714
1922
|
const message = error instanceof Error ? error.message : String(error);
|
|
1715
|
-
throw new Error(
|
|
1923
|
+
throw new Error(
|
|
1924
|
+
`OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON must be valid JSON: ${message}`,
|
|
1925
|
+
{ cause: error },
|
|
1926
|
+
);
|
|
1716
1927
|
}
|
|
1717
1928
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1718
|
-
throw new Error(
|
|
1929
|
+
throw new Error(
|
|
1930
|
+
"OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON must be a JSON object keyed by backend name",
|
|
1931
|
+
);
|
|
1719
1932
|
}
|
|
1720
1933
|
const out: Record<string, number> = {};
|
|
1721
1934
|
for (const [backend, value] of Object.entries(parsed)) {
|
|
1722
1935
|
if (!backend.trim()) {
|
|
1723
|
-
throw new Error(
|
|
1936
|
+
throw new Error(
|
|
1937
|
+
"OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON contains an empty backend name",
|
|
1938
|
+
);
|
|
1724
1939
|
}
|
|
1725
1940
|
const rate = typeof value === "number" ? value : Number(value);
|
|
1726
1941
|
if (!Number.isFinite(rate) || rate < 0) {
|
|
1727
|
-
throw new Error(
|
|
1942
|
+
throw new Error(
|
|
1943
|
+
`OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON rate for ${backend} must be a non-negative number`,
|
|
1944
|
+
);
|
|
1728
1945
|
}
|
|
1729
1946
|
out[backend] = rate;
|
|
1730
1947
|
}
|
|
@@ -1752,7 +1969,9 @@ export function parseModelProvidersJson(raw: string): RegistryProvider[] {
|
|
|
1752
1969
|
parsed = JSON.parse(raw);
|
|
1753
1970
|
} catch (error) {
|
|
1754
1971
|
const message = error instanceof Error ? error.message : String(error);
|
|
1755
|
-
throw new Error(`OPENGENI_MODEL_PROVIDERS_JSON must be valid JSON: ${message}
|
|
1972
|
+
throw new Error(`OPENGENI_MODEL_PROVIDERS_JSON must be valid JSON: ${message}`, {
|
|
1973
|
+
cause: error,
|
|
1974
|
+
});
|
|
1756
1975
|
}
|
|
1757
1976
|
if (!Array.isArray(parsed)) {
|
|
1758
1977
|
throw new Error("OPENGENI_MODEL_PROVIDERS_JSON must be a JSON array of providers");
|
|
@@ -1760,13 +1979,17 @@ export function parseModelProvidersJson(raw: string): RegistryProvider[] {
|
|
|
1760
1979
|
return parsed.map((entry, index) => {
|
|
1761
1980
|
const result = RegistryProviderSchema.safeParse(entry);
|
|
1762
1981
|
if (!result.success) {
|
|
1763
|
-
throw new Error(
|
|
1982
|
+
throw new Error(
|
|
1983
|
+
`OPENGENI_MODEL_PROVIDERS_JSON provider[${index}] is invalid: ${result.error.message}`,
|
|
1984
|
+
);
|
|
1764
1985
|
}
|
|
1765
1986
|
return result.data;
|
|
1766
1987
|
});
|
|
1767
1988
|
}
|
|
1768
1989
|
|
|
1769
|
-
export function parseIntegrationsOauthClientsJson(
|
|
1990
|
+
export function parseIntegrationsOauthClientsJson(
|
|
1991
|
+
raw: string | undefined,
|
|
1992
|
+
): Record<string, IntegrationOAuthClientConfig> {
|
|
1770
1993
|
if (!raw?.trim() || raw.trim() === "{}") {
|
|
1771
1994
|
return {};
|
|
1772
1995
|
}
|
|
@@ -1775,10 +1998,14 @@ export function parseIntegrationsOauthClientsJson(raw: string | undefined): Reco
|
|
|
1775
1998
|
parsed = JSON.parse(raw);
|
|
1776
1999
|
} catch (error) {
|
|
1777
2000
|
const message = error instanceof Error ? error.message : String(error);
|
|
1778
|
-
throw new Error(`OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON must be valid JSON: ${message}
|
|
2001
|
+
throw new Error(`OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON must be valid JSON: ${message}`, {
|
|
2002
|
+
cause: error,
|
|
2003
|
+
});
|
|
1779
2004
|
}
|
|
1780
2005
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1781
|
-
throw new Error(
|
|
2006
|
+
throw new Error(
|
|
2007
|
+
"OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON must be a JSON object keyed by authorization-server issuer or URL",
|
|
2008
|
+
);
|
|
1782
2009
|
}
|
|
1783
2010
|
const out: Record<string, IntegrationOAuthClientConfig> = {};
|
|
1784
2011
|
for (const [key, value] of Object.entries(parsed)) {
|
|
@@ -1787,7 +2014,9 @@ export function parseIntegrationsOauthClientsJson(raw: string | undefined): Reco
|
|
|
1787
2014
|
}
|
|
1788
2015
|
const result = IntegrationOAuthClientConfigSchema.safeParse(value);
|
|
1789
2016
|
if (!result.success) {
|
|
1790
|
-
throw new Error(
|
|
2017
|
+
throw new Error(
|
|
2018
|
+
`OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON client for ${key} is invalid: ${result.error.message}`,
|
|
2019
|
+
);
|
|
1791
2020
|
}
|
|
1792
2021
|
out[key] = result.data;
|
|
1793
2022
|
}
|
|
@@ -1803,7 +2032,9 @@ export function parseStaticUsageLimitsJson(raw: string): StaticUsageLimitsConfig
|
|
|
1803
2032
|
parsed = JSON.parse(raw);
|
|
1804
2033
|
} catch (error) {
|
|
1805
2034
|
const message = error instanceof Error ? error.message : String(error);
|
|
1806
|
-
throw new Error(`OPENGENI_STATIC_USAGE_LIMITS_JSON must be valid JSON: ${message}
|
|
2035
|
+
throw new Error(`OPENGENI_STATIC_USAGE_LIMITS_JSON must be valid JSON: ${message}`, {
|
|
2036
|
+
cause: error,
|
|
2037
|
+
});
|
|
1807
2038
|
}
|
|
1808
2039
|
return StaticUsageLimits.parse(parsed);
|
|
1809
2040
|
}
|
|
@@ -1817,7 +2048,9 @@ export function parseStaticEntitlementsJson(raw: string): EntitlementsConfig {
|
|
|
1817
2048
|
parsed = JSON.parse(raw);
|
|
1818
2049
|
} catch (error) {
|
|
1819
2050
|
const message = error instanceof Error ? error.message : String(error);
|
|
1820
|
-
throw new Error(`OPENGENI_STATIC_ENTITLEMENTS_JSON must be valid JSON: ${message}
|
|
2051
|
+
throw new Error(`OPENGENI_STATIC_ENTITLEMENTS_JSON must be valid JSON: ${message}`, {
|
|
2052
|
+
cause: error,
|
|
2053
|
+
});
|
|
1821
2054
|
}
|
|
1822
2055
|
return Entitlements.parse(parsed);
|
|
1823
2056
|
}
|
|
@@ -1827,10 +2060,13 @@ function calculateEntryCostMicros(pricing: ModelPricing, entry: ModelUsageInput)
|
|
|
1827
2060
|
const outputTokens = positiveInt(entry.outputTokens);
|
|
1828
2061
|
const cachedTokens = Math.min(inputTokens, cachedInputTokens(entry));
|
|
1829
2062
|
const uncachedInputTokens = Math.max(0, inputTokens - cachedTokens);
|
|
1830
|
-
const cachedInputRate =
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2063
|
+
const cachedInputRate =
|
|
2064
|
+
pricing.cachedInputMicrosPerMillionTokens ?? pricing.inputMicrosPerMillionTokens;
|
|
2065
|
+
return (
|
|
2066
|
+
Math.ceil((uncachedInputTokens * pricing.inputMicrosPerMillionTokens) / 1_000_000) +
|
|
2067
|
+
Math.ceil((cachedTokens * cachedInputRate) / 1_000_000) +
|
|
2068
|
+
Math.ceil((outputTokens * pricing.outputMicrosPerMillionTokens) / 1_000_000)
|
|
2069
|
+
);
|
|
1834
2070
|
}
|
|
1835
2071
|
|
|
1836
2072
|
function cachedInputTokens(entry: ModelUsageInput): number {
|
|
@@ -1841,9 +2077,10 @@ function cachedInputTokens(entry: ModelUsageInput): number {
|
|
|
1841
2077
|
: [];
|
|
1842
2078
|
let total = 0;
|
|
1843
2079
|
for (const detail of details) {
|
|
1844
|
-
total +=
|
|
1845
|
-
|
|
1846
|
-
|
|
2080
|
+
total +=
|
|
2081
|
+
positiveInt(detail.cached_tokens) +
|
|
2082
|
+
positiveInt(detail.cachedInputTokens) +
|
|
2083
|
+
positiveInt(detail.cached_input_tokens);
|
|
1847
2084
|
}
|
|
1848
2085
|
return total;
|
|
1849
2086
|
}
|
|
@@ -1876,20 +2113,36 @@ function ensureBuiltInMcpServers(settings: Settings): Settings["mcpServers"] {
|
|
|
1876
2113
|
// safe to cache / leave as-is.)
|
|
1877
2114
|
cacheToolsList: false,
|
|
1878
2115
|
},
|
|
1879
|
-
...(hasFiles
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
2116
|
+
...(hasFiles
|
|
2117
|
+
? []
|
|
2118
|
+
: [
|
|
2119
|
+
{
|
|
2120
|
+
id: "files",
|
|
2121
|
+
name: "Files",
|
|
2122
|
+
url: firstPartyMcpUrl,
|
|
2123
|
+
allowedTools: ["files_get_download_url"],
|
|
2124
|
+
cacheToolsList: true,
|
|
2125
|
+
},
|
|
2126
|
+
]),
|
|
2127
|
+
...(hasDocs
|
|
2128
|
+
? []
|
|
2129
|
+
: [
|
|
2130
|
+
{
|
|
2131
|
+
id: "docs",
|
|
2132
|
+
name: "Document Search",
|
|
2133
|
+
url: firstPartyDocsMcpUrl,
|
|
2134
|
+
allowedTools: [
|
|
2135
|
+
"search_documents",
|
|
2136
|
+
"fetch_document_chunk",
|
|
2137
|
+
"list_document_bases",
|
|
2138
|
+
"knowledge_search",
|
|
2139
|
+
"knowledge_fetch",
|
|
2140
|
+
"memory_search",
|
|
2141
|
+
"memory_propose",
|
|
2142
|
+
],
|
|
2143
|
+
cacheToolsList: false,
|
|
2144
|
+
},
|
|
2145
|
+
]),
|
|
1893
2146
|
...existing,
|
|
1894
2147
|
];
|
|
1895
2148
|
}
|
|
@@ -1916,7 +2169,10 @@ function ensureBuiltInMcpServers(settings: Settings): Settings["mcpServers"] {
|
|
|
1916
2169
|
* the one binding a mounted embed cannot leave unset.
|
|
1917
2170
|
*/
|
|
1918
2171
|
export function firstPartyMcpBaseUrl(settings: Settings): string {
|
|
1919
|
-
return
|
|
2172
|
+
return (
|
|
2173
|
+
settings.opengeniMcpUrl ??
|
|
2174
|
+
`http://127.0.0.1:${settings.apiPort}/v1/workspaces/{workspaceId}/mcp`
|
|
2175
|
+
);
|
|
1920
2176
|
}
|
|
1921
2177
|
|
|
1922
2178
|
export function firstPartyMcpWorkspaceUrl(settings: Settings, workspaceId: string): string {
|
|
@@ -1945,45 +2201,67 @@ function validateSettings(settings: Settings): void {
|
|
|
1945
2201
|
}
|
|
1946
2202
|
if (settings.productAccessMode === "managed") {
|
|
1947
2203
|
if (!settings.publicBaseUrl) {
|
|
1948
|
-
throw new Error(
|
|
2204
|
+
throw new Error(
|
|
2205
|
+
"OPENGENI_PUBLIC_BASE_URL is required when OPENGENI_PRODUCT_ACCESS_MODE=managed",
|
|
2206
|
+
);
|
|
1949
2207
|
}
|
|
1950
2208
|
if (!settings.betterAuthSecret) {
|
|
1951
|
-
throw new Error(
|
|
2209
|
+
throw new Error(
|
|
2210
|
+
"OPENGENI_BETTER_AUTH_SECRET is required when OPENGENI_PRODUCT_ACCESS_MODE=managed",
|
|
2211
|
+
);
|
|
1952
2212
|
}
|
|
1953
2213
|
if (!settings.delegationSecret) {
|
|
1954
|
-
throw new Error(
|
|
2214
|
+
throw new Error(
|
|
2215
|
+
"OPENGENI_DELEGATION_SECRET is required when OPENGENI_PRODUCT_ACCESS_MODE=managed",
|
|
2216
|
+
);
|
|
1955
2217
|
}
|
|
1956
2218
|
if (!["local", "test"].includes(settings.environment) && !settings.resendApiKey) {
|
|
1957
2219
|
throw new Error("OPENGENI_RESEND_API_KEY is required for managed mode outside local/test");
|
|
1958
2220
|
}
|
|
1959
2221
|
if (!["local", "test"].includes(settings.environment) && !settings.environmentsEncryptionKey) {
|
|
1960
|
-
throw new Error(
|
|
2222
|
+
throw new Error(
|
|
2223
|
+
"OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is required for managed mode outside local/test",
|
|
2224
|
+
);
|
|
1961
2225
|
}
|
|
1962
2226
|
}
|
|
1963
2227
|
environmentsEncryptionKeyBytes(settings);
|
|
1964
2228
|
if (settings.integrationsEnabled) {
|
|
1965
2229
|
if (settings.productAccessMode === "managed" && !settings.publicBaseUrl) {
|
|
1966
|
-
throw new Error(
|
|
2230
|
+
throw new Error(
|
|
2231
|
+
"OPENGENI_PUBLIC_BASE_URL is required when OPENGENI_INTEGRATIONS_ENABLED=true and OPENGENI_PRODUCT_ACCESS_MODE=managed",
|
|
2232
|
+
);
|
|
1967
2233
|
}
|
|
1968
|
-
if (
|
|
1969
|
-
|
|
2234
|
+
if (
|
|
2235
|
+
settings.publicBaseUrl &&
|
|
2236
|
+
!settings.publicBaseUrl.startsWith("https://") &&
|
|
2237
|
+
!["local", "test"].includes(settings.environment)
|
|
2238
|
+
) {
|
|
2239
|
+
throw new Error(
|
|
2240
|
+
"OPENGENI_PUBLIC_BASE_URL must use https when OPENGENI_INTEGRATIONS_ENABLED=true outside local/test",
|
|
2241
|
+
);
|
|
1970
2242
|
}
|
|
1971
2243
|
if (!settings.integrationsStateSecret && !["local", "test"].includes(settings.environment)) {
|
|
1972
|
-
throw new Error(
|
|
2244
|
+
throw new Error(
|
|
2245
|
+
"OPENGENI_INTEGRATIONS_STATE_SECRET is required when OPENGENI_INTEGRATIONS_ENABLED=true outside local/test",
|
|
2246
|
+
);
|
|
1973
2247
|
}
|
|
1974
2248
|
}
|
|
1975
2249
|
parseIntegrationsOauthClientsJson(settings.integrationsOauthClientsJson);
|
|
1976
2250
|
if (
|
|
1977
|
-
settings.productAccessMode === "configured"
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2251
|
+
settings.productAccessMode === "configured" &&
|
|
2252
|
+
!["local", "test"].includes(settings.environment) &&
|
|
2253
|
+
!settings.delegationSecret &&
|
|
2254
|
+
!settings.authRequired
|
|
1981
2255
|
) {
|
|
1982
|
-
throw new Error(
|
|
2256
|
+
throw new Error(
|
|
2257
|
+
"OPENGENI_PRODUCT_ACCESS_MODE=configured requires OPENGENI_DELEGATION_SECRET or OPENGENI_AUTH_REQUIRED=true outside local/test",
|
|
2258
|
+
);
|
|
1983
2259
|
}
|
|
1984
2260
|
if (settings.billingMode === "stripe") {
|
|
1985
2261
|
if (!settings.stripeSecretKey || !settings.stripeWebhookSecret) {
|
|
1986
|
-
throw new Error(
|
|
2262
|
+
throw new Error(
|
|
2263
|
+
"OPENGENI_STRIPE_SECRET_KEY and OPENGENI_STRIPE_WEBHOOK_SECRET are required when OPENGENI_BILLING_MODE=stripe",
|
|
2264
|
+
);
|
|
1987
2265
|
}
|
|
1988
2266
|
}
|
|
1989
2267
|
if (settings.productAccessMode !== "managed" && settings.billingMode === "stripe") {
|
|
@@ -1993,13 +2271,17 @@ function validateSettings(settings: Settings): void {
|
|
|
1993
2271
|
const pricing = configuredModelPricing(settings);
|
|
1994
2272
|
const missing = configuredAllowedModels(settings).filter((model) => !pricing[model]);
|
|
1995
2273
|
if (missing.length > 0) {
|
|
1996
|
-
throw new Error(
|
|
2274
|
+
throw new Error(
|
|
2275
|
+
`Missing model pricing for managed billing model(s): ${missing.join(", ")}. Set OPENGENI_MODEL_PRICING_JSON.`,
|
|
2276
|
+
);
|
|
1997
2277
|
}
|
|
1998
2278
|
}
|
|
1999
2279
|
if (settings.usageLimitsMode === "static") {
|
|
2000
2280
|
const limits = configuredStaticUsageLimits(settings);
|
|
2001
2281
|
if (Object.keys(limits).length === 0) {
|
|
2002
|
-
throw new Error(
|
|
2282
|
+
throw new Error(
|
|
2283
|
+
"OPENGENI_STATIC_USAGE_LIMITS_JSON must define at least one cap when OPENGENI_USAGE_LIMITS_MODE=static",
|
|
2284
|
+
);
|
|
2003
2285
|
}
|
|
2004
2286
|
} else {
|
|
2005
2287
|
parseStaticUsageLimitsJson(settings.staticUsageLimitsJson);
|
|
@@ -2007,7 +2289,9 @@ function validateSettings(settings: Settings): void {
|
|
|
2007
2289
|
if (settings.entitlementsMode === "static") {
|
|
2008
2290
|
const entitlements = parseStaticEntitlementsJson(settings.staticEntitlementsJson);
|
|
2009
2291
|
if (Object.keys(entitlements).length === 0) {
|
|
2010
|
-
throw new Error(
|
|
2292
|
+
throw new Error(
|
|
2293
|
+
"OPENGENI_STATIC_ENTITLEMENTS_JSON must define at least one feature when OPENGENI_ENTITLEMENTS_MODE=static",
|
|
2294
|
+
);
|
|
2011
2295
|
}
|
|
2012
2296
|
} else {
|
|
2013
2297
|
parseStaticEntitlementsJson(settings.staticEntitlementsJson);
|
|
@@ -2017,7 +2301,9 @@ function validateSettings(settings: Settings): void {
|
|
|
2017
2301
|
}
|
|
2018
2302
|
if (settings.openaiProvider === "azure") {
|
|
2019
2303
|
if (!settings.azureOpenaiBaseUrl && !settings.azureOpenaiEndpoint) {
|
|
2020
|
-
throw new Error(
|
|
2304
|
+
throw new Error(
|
|
2305
|
+
"Azure OpenAI requires OPENGENI_AZURE_OPENAI_BASE_URL or OPENGENI_AZURE_OPENAI_ENDPOINT",
|
|
2306
|
+
);
|
|
2021
2307
|
}
|
|
2022
2308
|
if (!settings.azureOpenaiBaseUrl && !settings.azureOpenaiDeployment) {
|
|
2023
2309
|
throw new Error("Azure OpenAI endpoint mode requires OPENGENI_AZURE_OPENAI_DEPLOYMENT");
|
|
@@ -2033,7 +2319,9 @@ function validateSettings(settings: Settings): void {
|
|
|
2033
2319
|
// (a half-configured token is always a misconfiguration). This is orthogonal
|
|
2034
2320
|
// to the backend-gated required-cred sweep below.
|
|
2035
2321
|
if (Boolean(settings.modalTokenId) !== Boolean(settings.modalTokenSecret)) {
|
|
2036
|
-
throw new Error(
|
|
2322
|
+
throw new Error(
|
|
2323
|
+
"OPENGENI_MODAL_TOKEN_ID and OPENGENI_MODAL_TOKEN_SECRET must both be set or both omitted",
|
|
2324
|
+
);
|
|
2037
2325
|
}
|
|
2038
2326
|
// Backend-gated required credentials: only the *active* backend's creds are
|
|
2039
2327
|
// required. A modal deployment must carry the Modal token; a daytona/e2b/none
|
|
@@ -2041,48 +2329,115 @@ function validateSettings(settings: Settings): void {
|
|
|
2041
2329
|
// SANDBOX_REQUIRED_ENV table that the deployment package also mirrors.
|
|
2042
2330
|
for (const required of SANDBOX_REQUIRED_ENV[settings.sandboxBackend] ?? []) {
|
|
2043
2331
|
const value = settings[required.field];
|
|
2044
|
-
if (
|
|
2045
|
-
|
|
2332
|
+
if (
|
|
2333
|
+
value === undefined ||
|
|
2334
|
+
value === null ||
|
|
2335
|
+
(typeof value === "string" && value.trim().length === 0)
|
|
2336
|
+
) {
|
|
2337
|
+
throw new Error(
|
|
2338
|
+
`${required.env} is required when OPENGENI_SANDBOX_BACKEND=${settings.sandboxBackend}`,
|
|
2339
|
+
);
|
|
2046
2340
|
}
|
|
2047
2341
|
}
|
|
2048
|
-
if (
|
|
2049
|
-
|
|
2050
|
-
|
|
2342
|
+
if (
|
|
2343
|
+
settings.objectStorageBackend === "s3-compatible" ||
|
|
2344
|
+
settings.objectStorageBackend === "aws-s3"
|
|
2345
|
+
) {
|
|
2346
|
+
if (
|
|
2347
|
+
Boolean(settings.objectStorageAccessKeyId) !== Boolean(settings.objectStorageSecretAccessKey)
|
|
2348
|
+
) {
|
|
2349
|
+
throw new Error(
|
|
2350
|
+
"OPENGENI_OBJECT_STORAGE_ACCESS_KEY_ID and OPENGENI_OBJECT_STORAGE_SECRET_ACCESS_KEY must both be set or both omitted",
|
|
2351
|
+
);
|
|
2051
2352
|
}
|
|
2052
|
-
if (
|
|
2053
|
-
|
|
2353
|
+
if (
|
|
2354
|
+
settings.objectStorageBackend === "s3-compatible" &&
|
|
2355
|
+
(settings.objectStorageEndpoint || settings.objectStorageSandboxEndpoint) &&
|
|
2356
|
+
(!settings.objectStorageAccessKeyId || !settings.objectStorageSecretAccessKey)
|
|
2357
|
+
) {
|
|
2358
|
+
throw new Error(
|
|
2359
|
+
"S3-compatible object storage endpoints require OPENGENI_OBJECT_STORAGE_ACCESS_KEY_ID and OPENGENI_OBJECT_STORAGE_SECRET_ACCESS_KEY",
|
|
2360
|
+
);
|
|
2054
2361
|
}
|
|
2055
|
-
if (
|
|
2056
|
-
|
|
2362
|
+
if (
|
|
2363
|
+
settings.objectStorageAzureConnectionString ||
|
|
2364
|
+
settings.objectStorageAzureAccountName ||
|
|
2365
|
+
settings.objectStorageAzureAccountKey ||
|
|
2366
|
+
settings.objectStorageAzureEndpoint
|
|
2367
|
+
) {
|
|
2368
|
+
throw new Error(
|
|
2369
|
+
"S3 object storage uses OPENGENI_OBJECT_STORAGE_* S3 settings, not OPENGENI_OBJECT_STORAGE_AZURE_* settings",
|
|
2370
|
+
);
|
|
2057
2371
|
}
|
|
2058
|
-
if (
|
|
2059
|
-
|
|
2372
|
+
if (
|
|
2373
|
+
settings.objectStorageGcsProjectId ||
|
|
2374
|
+
settings.objectStorageGcsCredentialsJson ||
|
|
2375
|
+
settings.objectStorageGcsKeyFilename ||
|
|
2376
|
+
settings.objectStorageGcsApiEndpoint
|
|
2377
|
+
) {
|
|
2378
|
+
throw new Error(
|
|
2379
|
+
"S3 object storage uses OPENGENI_OBJECT_STORAGE_* S3 settings, not OPENGENI_OBJECT_STORAGE_GCS_* settings",
|
|
2380
|
+
);
|
|
2060
2381
|
}
|
|
2061
2382
|
} else if (settings.objectStorageBackend === "azure-blob") {
|
|
2062
|
-
if (
|
|
2063
|
-
|
|
2383
|
+
if (
|
|
2384
|
+
settings.objectStorageEndpoint ||
|
|
2385
|
+
settings.objectStorageSandboxEndpoint ||
|
|
2386
|
+
settings.objectStorageAccessKeyId ||
|
|
2387
|
+
settings.objectStorageSecretAccessKey
|
|
2388
|
+
) {
|
|
2389
|
+
throw new Error(
|
|
2390
|
+
"Azure Blob storage uses OPENGENI_OBJECT_STORAGE_AZURE_* settings, not S3-compatible object storage settings",
|
|
2391
|
+
);
|
|
2064
2392
|
}
|
|
2065
|
-
if (
|
|
2066
|
-
|
|
2393
|
+
if (
|
|
2394
|
+
settings.objectStorageGcsProjectId ||
|
|
2395
|
+
settings.objectStorageGcsCredentialsJson ||
|
|
2396
|
+
settings.objectStorageGcsKeyFilename ||
|
|
2397
|
+
settings.objectStorageGcsApiEndpoint
|
|
2398
|
+
) {
|
|
2399
|
+
throw new Error(
|
|
2400
|
+
"Azure Blob storage uses OPENGENI_OBJECT_STORAGE_AZURE_* settings, not OPENGENI_OBJECT_STORAGE_GCS_* settings",
|
|
2401
|
+
);
|
|
2067
2402
|
}
|
|
2068
2403
|
const hasConnectionString = Boolean(settings.objectStorageAzureConnectionString);
|
|
2069
|
-
const hasSharedKey =
|
|
2404
|
+
const hasSharedKey =
|
|
2405
|
+
Boolean(settings.objectStorageAzureAccountName) &&
|
|
2406
|
+
Boolean(settings.objectStorageAzureAccountKey);
|
|
2070
2407
|
if (!hasConnectionString && !hasSharedKey) {
|
|
2071
|
-
throw new Error(
|
|
2408
|
+
throw new Error(
|
|
2409
|
+
"Azure Blob storage requires OPENGENI_OBJECT_STORAGE_AZURE_CONNECTION_STRING or OPENGENI_OBJECT_STORAGE_AZURE_ACCOUNT_NAME plus OPENGENI_OBJECT_STORAGE_AZURE_ACCOUNT_KEY",
|
|
2410
|
+
);
|
|
2072
2411
|
}
|
|
2073
2412
|
} else {
|
|
2074
|
-
if (
|
|
2075
|
-
|
|
2413
|
+
if (
|
|
2414
|
+
settings.objectStorageEndpoint ||
|
|
2415
|
+
settings.objectStorageSandboxEndpoint ||
|
|
2416
|
+
settings.objectStorageAccessKeyId ||
|
|
2417
|
+
settings.objectStorageSecretAccessKey
|
|
2418
|
+
) {
|
|
2419
|
+
throw new Error(
|
|
2420
|
+
"GCS object storage uses OPENGENI_OBJECT_STORAGE_GCS_* settings, not S3-compatible object storage settings",
|
|
2421
|
+
);
|
|
2076
2422
|
}
|
|
2077
|
-
if (
|
|
2078
|
-
|
|
2423
|
+
if (
|
|
2424
|
+
settings.objectStorageAzureConnectionString ||
|
|
2425
|
+
settings.objectStorageAzureAccountName ||
|
|
2426
|
+
settings.objectStorageAzureAccountKey ||
|
|
2427
|
+
settings.objectStorageAzureEndpoint
|
|
2428
|
+
) {
|
|
2429
|
+
throw new Error(
|
|
2430
|
+
"GCS object storage uses OPENGENI_OBJECT_STORAGE_GCS_* settings, not OPENGENI_OBJECT_STORAGE_AZURE_* settings",
|
|
2431
|
+
);
|
|
2079
2432
|
}
|
|
2080
2433
|
if (settings.objectStorageGcsCredentialsJson) {
|
|
2081
2434
|
parseGcsCredentialsJson(settings.objectStorageGcsCredentialsJson);
|
|
2082
2435
|
}
|
|
2083
2436
|
}
|
|
2084
2437
|
if (settings.documentChunkOverlap >= settings.documentChunkSize) {
|
|
2085
|
-
throw new Error(
|
|
2438
|
+
throw new Error(
|
|
2439
|
+
"OPENGENI_DOCUMENT_CHUNK_OVERLAP must be smaller than OPENGENI_DOCUMENT_CHUNK_SIZE",
|
|
2440
|
+
);
|
|
2086
2441
|
}
|
|
2087
2442
|
parseExposedPorts(settings.dockerExposedPorts);
|
|
2088
2443
|
sandboxEnvironmentVariableNames(settings);
|
|
@@ -2121,31 +2476,35 @@ function validateSettings(settings: Settings): void {
|
|
|
2121
2476
|
const idleTimeoutMs = effectiveModalIdleTimeoutSeconds(settings) * 1000;
|
|
2122
2477
|
if (!(reaperPeriod < viewerTtl)) {
|
|
2123
2478
|
throw new Error(
|
|
2124
|
-
`OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS (${reaperPeriod}) must be strictly less than `
|
|
2125
|
-
|
|
2126
|
-
|
|
2479
|
+
`OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS (${reaperPeriod}) must be strictly less than ` +
|
|
2480
|
+
`OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS (${viewerTtl}): the reaper must run more often ` +
|
|
2481
|
+
`than the TTL it polices, or stale viewer holders outlive a full reaper period.`,
|
|
2482
|
+
);
|
|
2127
2483
|
}
|
|
2128
2484
|
if (!(idleTimeoutMs <= providerLifetimeMs)) {
|
|
2129
2485
|
throw new Error(
|
|
2130
|
-
`OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS*1000 (${idleTimeoutMs}) must not exceed the hard provider `
|
|
2131
|
-
|
|
2132
|
-
|
|
2486
|
+
`OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS*1000 (${idleTimeoutMs}) must not exceed the hard provider ` +
|
|
2487
|
+
`lifetime (OPENGENI_MODAL_TIMEOUT_SECONDS*1000 = ${providerLifetimeMs}): the idle timeout is a ` +
|
|
2488
|
+
`floor under the hard lifetime, not above it.`,
|
|
2489
|
+
);
|
|
2133
2490
|
}
|
|
2134
2491
|
if (!(viewerTtl < idleTimeoutMs)) {
|
|
2135
2492
|
throw new Error(
|
|
2136
|
-
`OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS (${viewerTtl}) must be strictly less than the effective box `
|
|
2137
|
-
|
|
2138
|
-
|
|
2493
|
+
`OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS (${viewerTtl}) must be strictly less than the effective box ` +
|
|
2494
|
+
`idle timeout (${idleTimeoutMs}): a viewer holder must be reapable before the box idles out from ` +
|
|
2495
|
+
`under it (the provider idle-timeout is the backstop).`,
|
|
2496
|
+
);
|
|
2139
2497
|
}
|
|
2140
2498
|
if (!(reaperPeriod + idleGraceMs < idleTimeoutMs)) {
|
|
2141
2499
|
throw new Error(
|
|
2142
|
-
`OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS + OPENGENI_SANDBOX_IDLE_GRACE_MS `
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2500
|
+
`OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS + OPENGENI_SANDBOX_IDLE_GRACE_MS ` +
|
|
2501
|
+
`(${reaperPeriod} + ${idleGraceMs} = ${reaperPeriod + idleGraceMs}) must be strictly less than the ` +
|
|
2502
|
+
`effective box idle timeout (${idleTimeoutMs}): a drained box must SURVIVE its full warm window so ` +
|
|
2503
|
+
`the reaper can resume + snapshot /workspace + terminate it on the sweep AFTER the drain grace ` +
|
|
2504
|
+
`elapses — Modal's idle-reap must NOT fire first (or /workspace is lost). Raise ` +
|
|
2505
|
+
`OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS (defaults to OPENGENI_MODAL_TIMEOUT_SECONDS) or lower ` +
|
|
2506
|
+
`OPENGENI_SANDBOX_IDLE_GRACE_MS.`,
|
|
2507
|
+
);
|
|
2149
2508
|
}
|
|
2150
2509
|
}
|
|
2151
2510
|
// --- stream-token secret: required-when-desktop, but GRACEFULLY DEGRADE (I8) ---
|
|
@@ -2158,10 +2517,10 @@ function validateSettings(settings: Settings): void {
|
|
|
2158
2517
|
// crashing the whole API on a missing secret.
|
|
2159
2518
|
if (settings.sandboxDesktopEnabled && resolveStreamTokenSecret(settings) === undefined) {
|
|
2160
2519
|
console.warn(
|
|
2161
|
-
"[opengeni] OPENGENI_SANDBOX_DESKTOP_ENABLED=true but neither OPENGENI_STREAM_TOKEN_SECRET nor "
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2520
|
+
"[opengeni] OPENGENI_SANDBOX_DESKTOP_ENABLED=true but neither OPENGENI_STREAM_TOKEN_SECRET nor " +
|
|
2521
|
+
"OPENGENI_DELEGATION_SECRET is set: the desktop pixel plane will GRACEFULLY DEGRADE " +
|
|
2522
|
+
"(DesktopStream.transport=null — no scoped stream tokens can be minted). Set " +
|
|
2523
|
+
"OPENGENI_STREAM_TOKEN_SECRET to enable the live desktop stream.",
|
|
2165
2524
|
);
|
|
2166
2525
|
}
|
|
2167
2526
|
// Model provider registry: parse it here so JSON/zod errors surface at boot,
|
|
@@ -2176,14 +2535,20 @@ function validateSettings(settings: Settings): void {
|
|
|
2176
2535
|
const providerIds = new Set<string>();
|
|
2177
2536
|
for (const provider of registryProviders) {
|
|
2178
2537
|
if (provider.id === builtinId) {
|
|
2179
|
-
throw new Error(
|
|
2538
|
+
throw new Error(
|
|
2539
|
+
`OPENGENI_MODEL_PROVIDERS_JSON provider id ${provider.id} collides with the built-in provider id`,
|
|
2540
|
+
);
|
|
2180
2541
|
}
|
|
2181
2542
|
if (providerIds.has(provider.id)) {
|
|
2182
|
-
throw new Error(
|
|
2543
|
+
throw new Error(
|
|
2544
|
+
`OPENGENI_MODEL_PROVIDERS_JSON contains duplicate provider id ${provider.id}`,
|
|
2545
|
+
);
|
|
2183
2546
|
}
|
|
2184
2547
|
providerIds.add(provider.id);
|
|
2185
2548
|
if (!resolveProviderApiKey(provider)) {
|
|
2186
|
-
throw new Error(
|
|
2549
|
+
throw new Error(
|
|
2550
|
+
`OPENGENI_MODEL_PROVIDERS_JSON provider ${provider.id} requires a resolvable API key (set apiKey or apiKeyEnv)`,
|
|
2551
|
+
);
|
|
2187
2552
|
}
|
|
2188
2553
|
}
|
|
2189
2554
|
}
|
|
@@ -2303,7 +2668,10 @@ export function resolveNatsControlPlaneAuth(settings: Settings): NatsControlPlan
|
|
|
2303
2668
|
}
|
|
2304
2669
|
|
|
2305
2670
|
function splitCsv(raw: string): string[] {
|
|
2306
|
-
return raw
|
|
2671
|
+
return raw
|
|
2672
|
+
.split(",")
|
|
2673
|
+
.map((value) => value.trim())
|
|
2674
|
+
.filter(Boolean);
|
|
2307
2675
|
}
|
|
2308
2676
|
|
|
2309
2677
|
function uniqueEnvNames(raw: string[], fieldName: string): string[] {
|
|
@@ -2330,7 +2698,9 @@ function parseGcsCredentialsJson(raw: string): unknown {
|
|
|
2330
2698
|
return JSON.parse(raw);
|
|
2331
2699
|
} catch (error) {
|
|
2332
2700
|
const message = error instanceof Error ? error.message : String(error);
|
|
2333
|
-
throw new Error(`OPENGENI_OBJECT_STORAGE_GCS_CREDENTIALS_JSON must be valid JSON: ${message}
|
|
2701
|
+
throw new Error(`OPENGENI_OBJECT_STORAGE_GCS_CREDENTIALS_JSON must be valid JSON: ${message}`, {
|
|
2702
|
+
cause: error,
|
|
2703
|
+
});
|
|
2334
2704
|
}
|
|
2335
2705
|
}
|
|
2336
2706
|
|