@opengeni/config 0.4.0 → 0.5.0

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