@opengeni/config 0.16.4 → 0.19.1-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +150 -17
- package/dist/index.js +539 -69
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +736 -146
package/src/index.ts
CHANGED
|
@@ -4,12 +4,14 @@ import {
|
|
|
4
4
|
DEFAULT_FIRST_PARTY_MCP_TOOLS,
|
|
5
5
|
Entitlements,
|
|
6
6
|
EntitlementsMode,
|
|
7
|
+
KnowledgeSourceSyncLimits,
|
|
7
8
|
LatencyMode,
|
|
8
9
|
MAX_NESTED_AGENT_DEPTH,
|
|
9
10
|
ProductAccessMode,
|
|
10
11
|
ReasoningEffort,
|
|
11
12
|
FIRST_PARTY_MCP_TOOL_NAMES,
|
|
12
13
|
FirstPartyMcpToolName,
|
|
14
|
+
OpenGeniSlackBotDisplayName,
|
|
13
15
|
SandboxBackend,
|
|
14
16
|
SessionMcpApprovalPolicy,
|
|
15
17
|
SEEDANCE_2_5_MODEL_ID,
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
XAI_SUBSCRIPTION_MODEL_ID_PREFIX,
|
|
41
43
|
XAI_SUBSCRIPTION_PROVIDER_ID,
|
|
42
44
|
XAI_SUBSCRIPTION_PROXY_BASE_URL,
|
|
45
|
+
XAI_RESPONSE_STREAM_IDLE_TIMEOUT_MS,
|
|
43
46
|
} from "@opengeni/xai-subscription";
|
|
44
47
|
export { XAI_SUBSCRIPTION_MODEL_ID_PREFIX } from "@opengeni/xai-subscription";
|
|
45
48
|
import { createHash } from "node:crypto";
|
|
@@ -55,6 +58,8 @@ export const SANDBOX_ARCHIVE_CAPTURE_MAX_TIMEOUT_MS = 60 * 60_000;
|
|
|
55
58
|
export const SANDBOX_ARCHIVE_CAPTURE_SETTLEMENT_GRACE_MS = 10_000;
|
|
56
59
|
export const SANDBOX_SNAPSHOT_MAX_TIMEOUT_MS =
|
|
57
60
|
SANDBOX_ARCHIVE_CAPTURE_MAX_TIMEOUT_MS - SANDBOX_ARCHIVE_CAPTURE_SETTLEMENT_GRACE_MS;
|
|
61
|
+
export const GOOGLE_DRIVE_PROVIDER_REQUEST_TIMEOUT_MAX_MS = 60_000;
|
|
62
|
+
export const GOOGLE_DRIVE_PROVIDER_RETRY_DELAY_MAX_MS = 60_000;
|
|
58
63
|
// Admission waits are observational: successful capture/teardown returns as
|
|
59
64
|
// soon as the DB fence clears. This ceiling only covers the unhealthy path. It
|
|
60
65
|
// allows one scheduled inventory and one complete successor claim without
|
|
@@ -79,6 +84,20 @@ const EnvBoolean = z.preprocess((value) => {
|
|
|
79
84
|
return value;
|
|
80
85
|
}, z.boolean());
|
|
81
86
|
|
|
87
|
+
/** Default pacing between consecutive no-input goal continuations. */
|
|
88
|
+
export const DEFAULT_GOAL_IDLE_BACKOFF_MS: readonly number[] = [3_000, 30_000, 120_000, 300_000];
|
|
89
|
+
export const DEFAULT_GOAL_IDLE_BACKOFF_MAX_MS = 600_000;
|
|
90
|
+
|
|
91
|
+
const EnvGoalIdleBackoffMs = z.preprocess((value) => {
|
|
92
|
+
if (typeof value !== "string") return value;
|
|
93
|
+
const source = value.trim();
|
|
94
|
+
if (!source) return undefined;
|
|
95
|
+
return source.split(",").map((entry) => {
|
|
96
|
+
const trimmed = entry.trim();
|
|
97
|
+
return trimmed === "" ? Number.NaN : Number(trimmed);
|
|
98
|
+
});
|
|
99
|
+
}, z.array(z.number().int().nonnegative()).min(1, "OPENGENI_GOAL_IDLE_BACKOFF_MS must list at least one delay in milliseconds").readonly());
|
|
100
|
+
|
|
82
101
|
const EnvFirstPartyMcpTools = z.preprocess(
|
|
83
102
|
(value) => {
|
|
84
103
|
if (typeof value !== "string") return value;
|
|
@@ -321,7 +340,7 @@ const SettingsSchema = z.object({
|
|
|
321
340
|
agentStableVersion: z
|
|
322
341
|
.string()
|
|
323
342
|
.regex(/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u)
|
|
324
|
-
.default("0.1.
|
|
343
|
+
.default("0.1.16"),
|
|
325
344
|
// Optional independent beta-channel pointer. When unset, the beta update
|
|
326
345
|
// manifest route is unavailable rather than silently serving stable.
|
|
327
346
|
agentBetaVersion: z
|
|
@@ -329,6 +348,26 @@ const SettingsSchema = z.object({
|
|
|
329
348
|
.regex(/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u)
|
|
330
349
|
.optional(),
|
|
331
350
|
productAccessMode: ProductAccessMode.default("local"),
|
|
351
|
+
// --- canonical organization-tenancy authority activation, default OFF ---
|
|
352
|
+
// The named PRE-ACTIVATION opt-out for the organization-tenancy program. FALSE (the
|
|
353
|
+
// default, and the value an operator leaves in place to decline or defer) means
|
|
354
|
+
// this deployment stays on the reversible legacy workspace-owned lane: no phase-F
|
|
355
|
+
// subsystem may switch its access decision to organization/membership authority
|
|
356
|
+
// ids. TRUE is an operator's explicit statement that the activation preconditions
|
|
357
|
+
// in docs/organization-tenancy.md have been proven for this deployment and that
|
|
358
|
+
// the one-way boundary is accepted.
|
|
359
|
+
//
|
|
360
|
+
// This is NOT a kill switch and NOT a rollback: once an activation migration has
|
|
361
|
+
// committed, setting it back to false does not restore the legacy authority - only
|
|
362
|
+
// forward recovery is available. It also grants and revokes nothing by itself;
|
|
363
|
+
// every individual authorization decision keeps its own fences.
|
|
364
|
+
//
|
|
365
|
+
// No runtime path reads it yet: canonical activation (phase F) is unshipped, so
|
|
366
|
+
// the flag exists to reserve the name, pin the safe default, and give every future
|
|
367
|
+
// activation slice one gate to consult. EnvBoolean (NOT z.coerce.boolean(), which
|
|
368
|
+
// coerces "false" -> true and would activate the moment an operator wrote the
|
|
369
|
+
// variable out to disable it).
|
|
370
|
+
organizationTenancyCanonicalActivationEnabled: EnvBoolean.default(false),
|
|
332
371
|
billingMode: BillingMode.default("disabled"),
|
|
333
372
|
entitlementsMode: EntitlementsMode.default("none"),
|
|
334
373
|
usageLimitsMode: UsageLimitsMode.default("none"),
|
|
@@ -347,7 +386,6 @@ const SettingsSchema = z.object({
|
|
|
347
386
|
// holder of stream:control gets 403 until this flips. Keeps stream:control a
|
|
348
387
|
// declared-but-inert permission so later hardening is a flag flip.
|
|
349
388
|
streamControlEnabled: EnvBoolean.default(false),
|
|
350
|
-
codemodeMaxCallsPerTurn: z.coerce.number().int().positive().default(200),
|
|
351
389
|
// Optional release-coherent bootstrap hint for custom rigs/connected machines
|
|
352
390
|
// that do not carry the stock-image ogtool binary. Exact stable versions only:
|
|
353
391
|
// the agent must never guess a tag or silently install `latest`.
|
|
@@ -360,12 +398,53 @@ const SettingsSchema = z.object({
|
|
|
360
398
|
integrationsStateSecret: z.string().optional(),
|
|
361
399
|
integrationsAllowPrivateNetworkTargets: EnvBoolean.default(false),
|
|
362
400
|
integrationsOauthClientsJson: z.string().default("{}"),
|
|
363
|
-
gmailRestAdapterEnabled: EnvBoolean.default(false),
|
|
364
401
|
slackClientId: z.string().optional(),
|
|
365
402
|
slackClientSecret: z.string().optional(),
|
|
366
403
|
slackSigningSecret: z.string().optional(),
|
|
404
|
+
slackBotDisplayName: OpenGeniSlackBotDisplayName.default("OpenGeni"),
|
|
405
|
+
slackCommand: z
|
|
406
|
+
.string()
|
|
407
|
+
.trim()
|
|
408
|
+
.regex(/^\/[a-z0-9_-]{1,31}$/u)
|
|
409
|
+
.default("/opengeni"),
|
|
367
410
|
googleDriveClientId: z.string().optional(),
|
|
368
411
|
googleDriveClientSecret: z.string().optional(),
|
|
412
|
+
googleDriveSyncMaxItems: z.coerce.number().int().positive().max(10_000).default(500),
|
|
413
|
+
googleDriveSyncMaxBytes: z.coerce
|
|
414
|
+
.number()
|
|
415
|
+
.int()
|
|
416
|
+
.positive()
|
|
417
|
+
.max(5_000_000_000)
|
|
418
|
+
.default(500_000_000),
|
|
419
|
+
googleDriveSyncMaxFileBytes: z.coerce
|
|
420
|
+
.number()
|
|
421
|
+
.int()
|
|
422
|
+
.positive()
|
|
423
|
+
.max(5_000_000_000)
|
|
424
|
+
.default(100_000_000),
|
|
425
|
+
googleDriveSyncMaxProviderRequests: z.coerce.number().int().positive().max(10_000).default(1_000),
|
|
426
|
+
googleDriveSyncMaxElapsedSeconds: z.coerce.number().int().positive().max(3_600).default(300),
|
|
427
|
+
googleDriveSyncMaxFailureDetails: z.coerce.number().int().positive().max(100).default(25),
|
|
428
|
+
googleDriveProviderRequestTimeoutMs: z.coerce
|
|
429
|
+
.number()
|
|
430
|
+
.int()
|
|
431
|
+
.min(1_000)
|
|
432
|
+
.max(GOOGLE_DRIVE_PROVIDER_REQUEST_TIMEOUT_MAX_MS)
|
|
433
|
+
.default(30_000),
|
|
434
|
+
googleDriveProviderRetryAttempts: z.coerce.number().int().min(1).max(5).default(3),
|
|
435
|
+
googleDriveProviderRetryInitialDelayMs: z.coerce
|
|
436
|
+
.number()
|
|
437
|
+
.int()
|
|
438
|
+
.positive()
|
|
439
|
+
.max(30_000)
|
|
440
|
+
.default(250),
|
|
441
|
+
googleDriveProviderRetryMaxDelayMs: z.coerce
|
|
442
|
+
.number()
|
|
443
|
+
.int()
|
|
444
|
+
.positive()
|
|
445
|
+
.max(GOOGLE_DRIVE_PROVIDER_RETRY_DELAY_MAX_MS)
|
|
446
|
+
.default(5_000),
|
|
447
|
+
googleDriveProviderRetryBudgetMs: z.coerce.number().int().positive().max(120_000).default(15_000),
|
|
369
448
|
fikenClientId: z.string().optional(),
|
|
370
449
|
fikenClientSecret: z.string().optional(),
|
|
371
450
|
googleDriveWorkspaceEventsEnabled: EnvBoolean.optional(),
|
|
@@ -378,24 +457,60 @@ const SettingsSchema = z.object({
|
|
|
378
457
|
// id ("x", "reddit"): {"x":{"clientId":"...","clientSecret":"..."}}.
|
|
379
458
|
socialOauthClientsJson: z.string().default("{}"),
|
|
380
459
|
// Session goal guard rails. Goals are designed for runs that legitimately
|
|
381
|
-
// span days, so length is bounded by
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
460
|
+
// span days, so length is bounded by explicit completion/pause and budget
|
|
461
|
+
// exhaustion, never by count. goalMaxAutoContinuations is therefore UNSET
|
|
462
|
+
// by default (no cap); deployments may configure one, and it then acts as a
|
|
463
|
+
// hard ceiling that per-goal overrides can only lower.
|
|
385
464
|
goalMaxAutoContinuations: z.coerce.number().int().positive().optional(),
|
|
386
|
-
|
|
465
|
+
// Idle backoff between CONSECUTIVE no-input goal continuations. This is
|
|
466
|
+
// pacing, not a cap: the first continuation after a turn that consumed any
|
|
467
|
+
// external input is immediate, the n-th consecutive no-input continuation
|
|
468
|
+
// waits schedule[min(n - 1, last)] ms after the previous one finished, and
|
|
469
|
+
// any new input (machine input, human/API prompt, Steer) wakes the session
|
|
470
|
+
// immediately. The delay never exceeds goalIdleBackoffMaxMs.
|
|
471
|
+
goalIdleBackoffMs: EnvGoalIdleBackoffMs.default(DEFAULT_GOAL_IDLE_BACKOFF_MS),
|
|
472
|
+
goalIdleBackoffMaxMs: z.coerce
|
|
473
|
+
.number()
|
|
474
|
+
.int()
|
|
475
|
+
.positive()
|
|
476
|
+
.default(DEFAULT_GOAL_IDLE_BACKOFF_MAX_MS),
|
|
477
|
+
// Child lifecycle notices: a child session's requires_action freeze, its
|
|
478
|
+
// resolution, a direct Pause, a provider-capacity wait, and goal progress
|
|
479
|
+
// become typed `session_system_updates` rows for the parent (in addition to
|
|
480
|
+
// `child_terminal_result`). Rolling hazard: a pre-notice worker throws on an
|
|
481
|
+
// unknown update kind, so enable only once the whole fleet runs an image
|
|
482
|
+
// that understands the new kinds. Once the flag has produced rows, a
|
|
483
|
+
// pre-notice image must never restart while any new-kind row is still
|
|
484
|
+
// pending (session_system_updates or session_system_update_outbox); turning
|
|
485
|
+
// the flag back off stops production but does not drain already committed
|
|
486
|
+
// rows. Default off. The API and both workers install the validated value
|
|
487
|
+
// into @opengeni/db once at boot.
|
|
488
|
+
// Env: OPENGENI_CHILD_LIFECYCLE_NOTICES_ENABLED.
|
|
489
|
+
childLifecycleNoticesEnabled: EnvBoolean.default(false),
|
|
490
|
+
// Per-channel and per-DM Slack workspace routing. Default ON. A channel does
|
|
491
|
+
// not count a personal workspace as a candidate, so an organization with one
|
|
492
|
+
// shared workspace resolves it as the sole candidate and never asks; the
|
|
493
|
+
// visible change is confined to organizations that genuinely have more than
|
|
494
|
+
// one - plus one case worth knowing before an upgrade: a person who has lost
|
|
495
|
+
// live authority now receives a posted refusal where the pre-routing code
|
|
496
|
+
// failed silently. Set the env var to `false` to restore the short-circuit
|
|
497
|
+
// to the installation's own workspace.
|
|
498
|
+
// Env: OPENGENI_SLACK_WORKSPACE_ROUTING_ENABLED.
|
|
499
|
+
slackWorkspaceRoutingEnabled: EnvBoolean.default(true),
|
|
387
500
|
// Per-segment ceiling on agent loop turns (model calls) within a single
|
|
388
501
|
// session turn. Effectively unbounded by default for the same reason as
|
|
389
502
|
// above; the graceful max-turns valve (idle + goal continuation, never a
|
|
390
503
|
// session failure) remains as inert safety should a deployment set a cap.
|
|
391
504
|
agentMaxModelCallsPerTurn: z.coerce.number().int().positive().default(1_000_000),
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
505
|
+
// Deployment fallback for models that do not declare their own window.
|
|
506
|
+
// Built-in billed GPT-5.6 Sol/Terra/Luna pin Codex's 272k catalog instead.
|
|
507
|
+
// OpenGeni always performs one durable, portable plaintext compaction
|
|
508
|
+
// transition; there is no provider/server/off mode ladder.
|
|
395
509
|
contextWindowTokens: z.coerce.number().int().positive().default(1_050_000),
|
|
396
|
-
// Optional model-catalog effective input ceiling. Codex
|
|
397
|
-
// raw context_window * effective_context_window_percent;
|
|
398
|
-
// the deployment-level window-minus-reserved-output
|
|
510
|
+
// Optional model-catalog effective input ceiling. Codex and billed GPT-5.6
|
|
511
|
+
// models expose this as raw context_window * effective_context_window_percent;
|
|
512
|
+
// when absent, retain the deployment-level window-minus-reserved-output
|
|
513
|
+
// behavior.
|
|
399
514
|
contextEffectiveWindowTokens: z.coerce.number().int().positive().optional(),
|
|
400
515
|
// Proactive compaction threshold as a ratio of the model context window.
|
|
401
516
|
// Defaults to 90%: compact as late as possible — retained context beats early
|
|
@@ -430,6 +545,10 @@ const SettingsSchema = z.object({
|
|
|
430
545
|
apiHost: z.string().default("0.0.0.0"),
|
|
431
546
|
apiPort: z.coerce.number().int().positive().default(8000),
|
|
432
547
|
workerHttpPort: z.coerce.number().int().positive().default(8001),
|
|
548
|
+
// Worker-side first-party MCP traffic stays on the deployment's internal
|
|
549
|
+
// network. OPENGENI_MCP_URL remains the sandbox/external route used by
|
|
550
|
+
// Codemode and remote placements.
|
|
551
|
+
opengeniMcpInternalUrl: z.string().url().optional(),
|
|
433
552
|
opengeniMcpUrl: z.string().url().optional(),
|
|
434
553
|
// Origins allowed to send browser cookies cross-origin. Other origins may
|
|
435
554
|
// call the public API with bearer credentials, but never receive credentialed
|
|
@@ -564,18 +683,27 @@ const SettingsSchema = z.object({
|
|
|
564
683
|
// SuperGrok/xAI connected subscription. This is a workspace-scoped OAuth
|
|
565
684
|
// account pool and a distinct rail from the existing xai/* API-key provider.
|
|
566
685
|
supergrokSubscriptionEnabled: EnvBoolean.default(false), // OPENGENI_SUPERGROK_SUBSCRIPTION_ENABLED
|
|
686
|
+
// Maximum silence between complete, valid SuperGrok SSE data events. This is
|
|
687
|
+
// not a request/run duration cap; every valid event resets the timer.
|
|
688
|
+
supergrokResponseStreamIdleTimeoutMs: z.coerce
|
|
689
|
+
.number()
|
|
690
|
+
.int()
|
|
691
|
+
.positive()
|
|
692
|
+
.max(24 * 60 * 60_000)
|
|
693
|
+
.default(XAI_RESPONSE_STREAM_IDLE_TIMEOUT_MS),
|
|
567
694
|
// Expose the connected apps attached to a Codex subscription through the
|
|
568
695
|
// synthetic codex_apps MCP server. Independent from subscription routing so
|
|
569
696
|
// operators can use Codex models without exposing ChatGPT connectors.
|
|
570
697
|
codexConnectedAppsEnabled: EnvBoolean.default(false), // OPENGENI_CODEX_CONNECTED_APPS_ENABLED
|
|
571
698
|
codexProductSku: z.string().optional(), // OPENGENI_CODEX_PRODUCT_SKU (X-OpenAI-Product-Sku, apps only)
|
|
572
699
|
// Progressive MCP disclosure (Codex-CLI-style tool_search): on a codex turn,
|
|
573
|
-
// flag non-
|
|
700
|
+
// flag non-eager selected MCP tools `defer_loading:true` (dropping their
|
|
574
701
|
// schemas from model context) and add one client-executed tool_search tool
|
|
575
|
-
// that BM25-discloses bounded matches.
|
|
576
|
-
// eager
|
|
577
|
-
//
|
|
578
|
-
//
|
|
702
|
+
// that BM25-discloses bounded matches. Only an exact session tool ref with
|
|
703
|
+
// `eager:true` stays on the startup path; mandatory selection alone does not
|
|
704
|
+
// imply eagerness. Default ON so selected connector catalogues do not consume
|
|
705
|
+
// every Codex turn's context. Operators may explicitly disable it for
|
|
706
|
+
// emergency compatibility diagnosis.
|
|
579
707
|
// OPENGENI_CODEX_TOOL_SEARCH_ENABLED
|
|
580
708
|
codexToolSearchEnabled: EnvBoolean.default(true),
|
|
581
709
|
// Provider-neutral progressive disclosure for direct OpenAI/Azure native
|
|
@@ -814,6 +942,39 @@ const SettingsSchema = z.object({
|
|
|
814
942
|
vercelProjectId: z.string().optional(),
|
|
815
943
|
vercelTeamId: z.string().optional(),
|
|
816
944
|
vercelRuntime: z.string().optional(),
|
|
945
|
+
// --- OpenSandbox (optional Kubernetes-native provisioned sandbox) ---
|
|
946
|
+
openSandboxBaseUrl: z.string().url().optional(),
|
|
947
|
+
openSandboxApiKey: z.string().min(1).optional(),
|
|
948
|
+
// Release and preview profiles must provide an immutable OCI digest. The
|
|
949
|
+
// adapter refuses tag-only references when this backend is active.
|
|
950
|
+
openSandboxImage: z.string().min(1).optional(),
|
|
951
|
+
// Renewable provider TTL is a leak/backstop clock, not OpenGeni's idle
|
|
952
|
+
// policy. The pinned server accepts a one-minute minimum; ordinary
|
|
953
|
+
// deployments default to one hour.
|
|
954
|
+
openSandboxTtlSeconds: z.coerce.number().int().min(60).max(86_400).default(3_600),
|
|
955
|
+
openSandboxUseServerProxy: EnvBoolean.default(true),
|
|
956
|
+
// Channel B (browserd / noVNC / ttyd) uses OSEP-0011 signed URI-mode ingress.
|
|
957
|
+
// Exec/files stay on the private lifecycle server-proxy regardless of this flag.
|
|
958
|
+
openSandboxSignedEndpoints: EnvBoolean.default(false),
|
|
959
|
+
openSandboxSignedEndpointTtlSeconds: z.coerce.number().int().min(60).max(3_600).default(600),
|
|
960
|
+
openSandboxChannelBPublicBaseUrl: z.string().url().optional(),
|
|
961
|
+
// Emergency hatch only: force JPEG/RFB through the API frame-proxy even when
|
|
962
|
+
// signed endpoints are on (M2 subprotocol failure). Unset means OpenSandbox
|
|
963
|
+
// uses the frame-proxy unless signed endpoints are on.
|
|
964
|
+
openSandboxInteractionFrameProxy: EnvBoolean.optional(),
|
|
965
|
+
openSandboxPoolRef: z
|
|
966
|
+
.string()
|
|
967
|
+
.regex(/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/)
|
|
968
|
+
.optional(),
|
|
969
|
+
// Optional same-cluster, read-only observability projection. The application
|
|
970
|
+
// chart sets this only on the control worker and mounts a dedicated projected
|
|
971
|
+
// service-account token; non-Kubernetes and remote-provider deployments omit it.
|
|
972
|
+
openSandboxKubernetesInventoryNamespace: z
|
|
973
|
+
.string()
|
|
974
|
+
.min(1)
|
|
975
|
+
.max(63)
|
|
976
|
+
.regex(/^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?$/)
|
|
977
|
+
.optional(),
|
|
817
978
|
// --- sandbox ownership inversion (P1.2 rollout flag, default OFF) ---
|
|
818
979
|
// The keystone flag for the stateless resume-by-id model. When FALSE the
|
|
819
980
|
// agent-turn path is BYTE-FOR-BYTE today's build-and-discard behavior (no
|
|
@@ -858,7 +1019,7 @@ const SettingsSchema = z.object({
|
|
|
858
1019
|
// runner must ALSO advertise Capabilities.op_stream. Streaming is the default
|
|
859
1020
|
// because it is the only transport that can keep a command alive without an
|
|
860
1021
|
// arbitrary request/reply wall while still supporting replay and cancellation.
|
|
861
|
-
//
|
|
1022
|
+
// Exec fails closed when the deployment or runner does not provide op-stream.
|
|
862
1023
|
// EnvBoolean (NOT
|
|
863
1024
|
// z.coerce.boolean(), which coerces "false" -> true).
|
|
864
1025
|
agentOpStreamEnabled: EnvBoolean.default(true),
|
|
@@ -890,9 +1051,10 @@ const SettingsSchema = z.object({
|
|
|
890
1051
|
// nats-server is configured with AUTH CALLOUT: an external agent connects
|
|
891
1052
|
// presenting its `oge_` enrollment bearer as the connect auth-token; the server
|
|
892
1053
|
// issues an authorization request on $SYS.REQ.USER.AUTH to our responder, which
|
|
893
|
-
// validates the bearer and returns a SIGNED NATS
|
|
894
|
-
//
|
|
895
|
-
//
|
|
1054
|
+
// validates the bearer, claims one daemon generation, and returns a SIGNED NATS
|
|
1055
|
+
// user JWT scoped to that exact process subtree (+ `_INBOX.>`). The exact scope
|
|
1056
|
+
// provides both workspace isolation and single-daemon routing authority. These
|
|
1057
|
+
// are deployment-level secrets in the opengeni-runtime secret
|
|
896
1058
|
// (Helm-clobbered configmap avoided), all OPTIONAL: when the callout plane is not
|
|
897
1059
|
// configured the responder simply does not start (selfhosted agents cannot
|
|
898
1060
|
// connect — graceful, never a boot-fail).
|
|
@@ -904,7 +1066,7 @@ const SettingsSchema = z.object({
|
|
|
904
1066
|
// The TARGET ACCOUNT NAME the minted user is placed into (the server-config-mode
|
|
905
1067
|
// `auth_callout.account`, e.g. "APP"). The responder writes it as the minted user
|
|
906
1068
|
// JWT `aud` so nats-server binds the agent to this account — the SAME account the
|
|
907
|
-
// privileged control plane connects into, so
|
|
1069
|
+
// privileged control plane connects into, so exact process request/reply
|
|
908
1070
|
// routes. Optional; resolveNatsCalloutConfig defaults it to "APP".
|
|
909
1071
|
selfhostedNatsCalloutAccountName: z.string().optional(),
|
|
910
1072
|
// The callout RESPONDER's own NATS login (one of the `auth_callout.auth_users`
|
|
@@ -913,7 +1075,7 @@ const SettingsSchema = z.object({
|
|
|
913
1075
|
selfhostedNatsCalloutUser: z.string().optional(),
|
|
914
1076
|
selfhostedNatsCalloutPassword: z.string().optional(),
|
|
915
1077
|
// The PRIVILEGED control-plane login (api/worker): a static account user that may
|
|
916
|
-
// request
|
|
1078
|
+
// request exact process RPC subjects + receive their inbox replies. The event bus + the
|
|
917
1079
|
// selfhosted control RPC ride THIS connection. Username/password; when unset the
|
|
918
1080
|
// bus connects anonymously (local dev / a NATS with no auth_callout).
|
|
919
1081
|
selfhostedNatsControlUser: z.string().optional(),
|
|
@@ -997,6 +1159,21 @@ const SettingsSchema = z.object({
|
|
|
997
1159
|
// (a liveness/reaper cadence), this bounds how long one turn waits for capacity
|
|
998
1160
|
// or provider creation before surfacing a clear turn.failed error.
|
|
999
1161
|
sandboxWarmingTimeoutMs: z.coerce.number().int().positive().default(600_000),
|
|
1162
|
+
// Request-scoped workspace control-prefix budget: how long one HTTP-originated
|
|
1163
|
+
// session/workspace mutation (Send, Steer, Pause/Resume/Cancel, queue
|
|
1164
|
+
// move/edit/delete, composer draft, settings narrowing, quiescent tree
|
|
1165
|
+
// deletion) may wait to enter the fair `workspace_inference_controls` prefix
|
|
1166
|
+
// before failing with the retryable 503 `WORKSPACE_CONTROL_BUSY`. Worker
|
|
1167
|
+
// settlement and claims never use it. The API installs the validated value
|
|
1168
|
+
// into @opengeni/db once at app construction; nothing reads the env per
|
|
1169
|
+
// request. Env: OPENGENI_WORKSPACE_CONTROL_LOCK_TIMEOUT_MS. Default 20 s.
|
|
1170
|
+
workspaceControlLockTimeoutMs: z.coerce
|
|
1171
|
+
.number({
|
|
1172
|
+
message: "OPENGENI_WORKSPACE_CONTROL_LOCK_TIMEOUT_MS must be a positive integer (ms)",
|
|
1173
|
+
})
|
|
1174
|
+
.int("OPENGENI_WORKSPACE_CONTROL_LOCK_TIMEOUT_MS must be a positive integer (ms)")
|
|
1175
|
+
.positive("OPENGENI_WORKSPACE_CONTROL_LOCK_TIMEOUT_MS must be a positive integer (ms)")
|
|
1176
|
+
.default(20_000),
|
|
1000
1177
|
// Rig setup-script budget (M3): the wall-clock timeout the rig-setup lifecycle
|
|
1001
1178
|
// hook runs its script under, distinct from the 120s per-command lifecycle
|
|
1002
1179
|
// default (a rig may compile/install heavy tooling on first cold create).
|
|
@@ -1056,6 +1233,11 @@ const SettingsSchema = z.object({
|
|
|
1056
1233
|
githubAppId: z.string().optional(),
|
|
1057
1234
|
githubClientId: z.string().optional(),
|
|
1058
1235
|
githubClientSecret: z.string().optional(),
|
|
1236
|
+
/** Default-off rollout for the in-process GitHub repository API tool surface. */
|
|
1237
|
+
githubRestMcpEnabled: EnvBoolean.default(false),
|
|
1238
|
+
githubPersonalOauthEnabled: EnvBoolean.default(false),
|
|
1239
|
+
githubPersonalOauthClientId: z.string().optional(),
|
|
1240
|
+
githubPersonalOauthClientSecret: z.string().optional(),
|
|
1059
1241
|
githubAppSlug: z.string().optional(),
|
|
1060
1242
|
githubWebhookSecret: z.string().optional(),
|
|
1061
1243
|
githubAppPrivateKey: z.string().optional(),
|
|
@@ -1096,6 +1278,76 @@ const SettingsSchema = z.object({
|
|
|
1096
1278
|
export type Settings = z.infer<typeof SettingsSchema>;
|
|
1097
1279
|
export type McpServerConfig = Settings["mcpServers"][number];
|
|
1098
1280
|
|
|
1281
|
+
export type GoogleDriveProviderRetryOptions = {
|
|
1282
|
+
requestTimeoutMs: number;
|
|
1283
|
+
attempts: number;
|
|
1284
|
+
initialDelayMs: number;
|
|
1285
|
+
maxDelayMs: number;
|
|
1286
|
+
budgetMs: number;
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
/** Freeze one validated provider-neutral budget into every newly created or
|
|
1290
|
+
* updated Google Drive knowledge-source schedule. Existing schedules retain
|
|
1291
|
+
* their persisted limits until an authorized source save updates them. */
|
|
1292
|
+
export function configuredGoogleDriveSyncLimits(settings: Settings) {
|
|
1293
|
+
return KnowledgeSourceSyncLimits.parse({
|
|
1294
|
+
maxItems: settings.googleDriveSyncMaxItems,
|
|
1295
|
+
maxBytes: settings.googleDriveSyncMaxBytes,
|
|
1296
|
+
maxFileBytes: settings.googleDriveSyncMaxFileBytes,
|
|
1297
|
+
maxProviderRequests: settings.googleDriveSyncMaxProviderRequests,
|
|
1298
|
+
maxElapsedSeconds: settings.googleDriveSyncMaxElapsedSeconds,
|
|
1299
|
+
maxFailureDetails: settings.googleDriveSyncMaxFailureDetails,
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/** Bounded in-activity retry policy for individual Google Drive requests. The
|
|
1304
|
+
* durable sync workflow remains authoritative after this local budget ends. */
|
|
1305
|
+
export function googleDriveProviderRetryOptions(
|
|
1306
|
+
settings: Settings,
|
|
1307
|
+
): GoogleDriveProviderRetryOptions {
|
|
1308
|
+
return {
|
|
1309
|
+
requestTimeoutMs: settings.googleDriveProviderRequestTimeoutMs,
|
|
1310
|
+
attempts: settings.googleDriveProviderRetryAttempts,
|
|
1311
|
+
initialDelayMs: settings.googleDriveProviderRetryInitialDelayMs,
|
|
1312
|
+
maxDelayMs: settings.googleDriveProviderRetryMaxDelayMs,
|
|
1313
|
+
budgetMs: settings.googleDriveProviderRetryBudgetMs,
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/** Return only a credential-free HTTP(S) origin. Reject path, user-info, query,
|
|
1318
|
+
* and fragment input instead of reflecting it into callbacks or evidence. */
|
|
1319
|
+
export function canonicalPublicOrigin(publicBaseUrl: string | undefined): string | null {
|
|
1320
|
+
if (!publicBaseUrl) return null;
|
|
1321
|
+
let parsed: URL;
|
|
1322
|
+
try {
|
|
1323
|
+
parsed = new URL(publicBaseUrl);
|
|
1324
|
+
} catch {
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
if (
|
|
1328
|
+
!["http:", "https:"].includes(parsed.protocol) ||
|
|
1329
|
+
parsed.username ||
|
|
1330
|
+
parsed.password ||
|
|
1331
|
+
parsed.search ||
|
|
1332
|
+
parsed.hash ||
|
|
1333
|
+
(parsed.pathname !== "" && parsed.pathname !== "/")
|
|
1334
|
+
) {
|
|
1335
|
+
return null;
|
|
1336
|
+
}
|
|
1337
|
+
return parsed.origin;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
export function googleDriveOAuthCallbackUrl(publicBaseUrl: string | undefined): string | null {
|
|
1341
|
+
const origin = canonicalPublicOrigin(publicBaseUrl);
|
|
1342
|
+
return origin ? `${origin}/v1/integrations/google-drive/callback` : null;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/** Exact callback registered on the environment-specific personal GitHub OAuth App. */
|
|
1346
|
+
export function personalGitHubOAuthCallbackUrl(publicBaseUrl: string | undefined): string | null {
|
|
1347
|
+
const origin = canonicalPublicOrigin(publicBaseUrl);
|
|
1348
|
+
return origin ? `${origin}/v1/integrations/github-personal/oauth/callback` : null;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1099
1351
|
/** Declarative voice-input transcription provider ids. */
|
|
1100
1352
|
export type VoiceInputProviderId =
|
|
1101
1353
|
| "openai"
|
|
@@ -1489,7 +1741,7 @@ export type ModelExecutionLimitsV1 = {
|
|
|
1489
1741
|
};
|
|
1490
1742
|
|
|
1491
1743
|
export type CredentialSourceV1 =
|
|
1492
|
-
| { kind: "deployment"; mechanism: "api_key" | "azure_ad_bearer" }
|
|
1744
|
+
| { kind: "deployment"; mechanism: "api_key" | "azure_ad_bearer" | "none" }
|
|
1493
1745
|
| { kind: "connected_subscription"; provider: "codex" | "xai" }
|
|
1494
1746
|
| { kind: "workspace_connection"; mechanism: "api_key" };
|
|
1495
1747
|
|
|
@@ -1508,13 +1760,23 @@ export type BillingAttributionV1 = {
|
|
|
1508
1760
|
export const ModelProviderApi = z.enum(["responses", "chat"]);
|
|
1509
1761
|
export type ModelProviderApi = z.infer<typeof ModelProviderApi>;
|
|
1510
1762
|
|
|
1763
|
+
/**
|
|
1764
|
+
* Provider-specific request semantics that are independent of the endpoint's
|
|
1765
|
+
* OpenAI-compatible wire API. Secondary Azure resources still speak the
|
|
1766
|
+
* Responses API, but need Azure's stricter computer-call normalization.
|
|
1767
|
+
*/
|
|
1768
|
+
export const ModelProviderWireProfile = z.enum(["openai", "azure-openai"]);
|
|
1769
|
+
export type ModelProviderWireProfile = z.infer<typeof ModelProviderWireProfile>;
|
|
1770
|
+
|
|
1511
1771
|
/**
|
|
1512
1772
|
* Registry provider kind. "api-key" providers carry their own static key/headers;
|
|
1513
|
-
*
|
|
1514
|
-
*
|
|
1773
|
+
* "anonymous" providers intentionally send no credential and are externally
|
|
1774
|
+
* metered; connected-subscription providers resolve a workspace account token
|
|
1775
|
+
* at call time and never carry a static key in the registry definition.
|
|
1515
1776
|
*/
|
|
1516
1777
|
export const RegistryProviderKind = z.enum([
|
|
1517
1778
|
"api-key",
|
|
1779
|
+
"anonymous",
|
|
1518
1780
|
"codex-subscription",
|
|
1519
1781
|
"xai-subscription",
|
|
1520
1782
|
"vercel-gateway-managed",
|
|
@@ -1572,24 +1834,73 @@ const RegistryModelSchema = z
|
|
|
1572
1834
|
});
|
|
1573
1835
|
|
|
1574
1836
|
/** A non-built-in provider declared by the host via OPENGENI_MODEL_PROVIDERS_JSON. */
|
|
1575
|
-
const RegistryProviderSchema = z
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1837
|
+
const RegistryProviderSchema = z
|
|
1838
|
+
.object({
|
|
1839
|
+
kind: RegistryProviderKind.default("api-key"),
|
|
1840
|
+
id: z.string().min(1).regex(registryId), // stable provider id, e.g. "fireworks"
|
|
1841
|
+
label: z.string().min(1).optional(),
|
|
1842
|
+
api: ModelProviderApi.default("chat"),
|
|
1843
|
+
wireProfile: ModelProviderWireProfile.default("openai"),
|
|
1844
|
+
baseUrl: z.string().url(),
|
|
1845
|
+
apiKey: z.string().optional(), // inline key (pragmatic) ...
|
|
1846
|
+
apiKeyEnv: z.string().optional(), // ... OR name of the env var holding the key (preferred)
|
|
1847
|
+
defaultQuery: z.record(z.string(), z.string()).optional(),
|
|
1848
|
+
defaultHeaders: z.record(z.string(), z.string()).optional(),
|
|
1849
|
+
publicDefaultQueryNames: z.array(z.string().min(1)).optional(),
|
|
1850
|
+
publicDefaultHeaderNames: z.array(z.string().min(1)).optional(),
|
|
1851
|
+
// V1 derives these from provider kind. Workspace BYOK is deliberately not a
|
|
1852
|
+
// registry switch and requires a separately reviewed encrypted broker.
|
|
1853
|
+
credentialSource: z.never().optional(),
|
|
1854
|
+
billing: z.never().optional(),
|
|
1855
|
+
models: z.array(RegistryModelSchema).min(1),
|
|
1856
|
+
})
|
|
1857
|
+
.superRefine((provider, ctx) => {
|
|
1858
|
+
if (provider.kind !== "anonymous") {
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if (provider.apiKey !== undefined) {
|
|
1862
|
+
ctx.addIssue({
|
|
1863
|
+
code: "custom",
|
|
1864
|
+
path: ["apiKey"],
|
|
1865
|
+
message: "anonymous providers must not declare apiKey",
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
if (provider.apiKeyEnv !== undefined) {
|
|
1869
|
+
ctx.addIssue({
|
|
1870
|
+
code: "custom",
|
|
1871
|
+
path: ["apiKeyEnv"],
|
|
1872
|
+
message: "anonymous providers must not declare apiKeyEnv",
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
if (provider.defaultHeaders !== undefined) {
|
|
1876
|
+
ctx.addIssue({
|
|
1877
|
+
code: "custom",
|
|
1878
|
+
path: ["defaultHeaders"],
|
|
1879
|
+
message: "anonymous providers must not declare defaultHeaders",
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
if (provider.defaultQuery !== undefined) {
|
|
1883
|
+
ctx.addIssue({
|
|
1884
|
+
code: "custom",
|
|
1885
|
+
path: ["defaultQuery"],
|
|
1886
|
+
message: "anonymous providers must not declare defaultQuery",
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
if (provider.publicDefaultHeaderNames !== undefined) {
|
|
1890
|
+
ctx.addIssue({
|
|
1891
|
+
code: "custom",
|
|
1892
|
+
path: ["publicDefaultHeaderNames"],
|
|
1893
|
+
message: "anonymous providers must not declare publicDefaultHeaderNames",
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
if (provider.publicDefaultQueryNames !== undefined) {
|
|
1897
|
+
ctx.addIssue({
|
|
1898
|
+
code: "custom",
|
|
1899
|
+
path: ["publicDefaultQueryNames"],
|
|
1900
|
+
message: "anonymous providers must not declare publicDefaultQueryNames",
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1593
1904
|
export type RegistryProvider = z.infer<typeof RegistryProviderSchema>;
|
|
1594
1905
|
|
|
1595
1906
|
export const IntegrationOAuthClientConfigSchema = z.object({
|
|
@@ -1611,8 +1922,9 @@ export type IntegrationOAuthClientConfig = z.infer<typeof IntegrationOAuthClient
|
|
|
1611
1922
|
export interface ResolvedModelProvider {
|
|
1612
1923
|
id: string; // "openai" | "azure" | registry id
|
|
1613
1924
|
label: string;
|
|
1614
|
-
kind: RegistryProviderKind; // "api-key" (built-ins + most registry) | "
|
|
1925
|
+
kind: RegistryProviderKind; // "api-key" (built-ins + most registry) | "anonymous" | subscription
|
|
1615
1926
|
api: ModelProviderApi;
|
|
1927
|
+
wireProfile: ModelProviderWireProfile;
|
|
1616
1928
|
builtin: boolean;
|
|
1617
1929
|
baseUrl?: string | undefined;
|
|
1618
1930
|
apiKey?: string | undefined;
|
|
@@ -1883,6 +2195,11 @@ export const SANDBOX_REQUIRED_ENV: Record<
|
|
|
1883
2195
|
{ field: "vercelToken", env: "OPENGENI_VERCEL_TOKEN" },
|
|
1884
2196
|
{ field: "vercelProjectId", env: "OPENGENI_VERCEL_PROJECT_ID" },
|
|
1885
2197
|
],
|
|
2198
|
+
opensandbox: [
|
|
2199
|
+
{ field: "openSandboxBaseUrl", env: "OPENGENI_OPENSANDBOX_BASE_URL" },
|
|
2200
|
+
{ field: "openSandboxApiKey", env: "OPENGENI_OPENSANDBOX_API_KEY" },
|
|
2201
|
+
{ field: "openSandboxImage", env: "OPENGENI_OPENSANDBOX_IMAGE" },
|
|
2202
|
+
],
|
|
1886
2203
|
// selfhosted needs NO per-box credentials: it is the user's own machine reached
|
|
1887
2204
|
// over the agent's own enrollment. The enrollment-signing + relay-token secrets
|
|
1888
2205
|
// are deployment-level (a single runtime secret, not per-active-backend creds),
|
|
@@ -1895,6 +2212,34 @@ export function requiredSandboxEnvForBackend(backend: z.infer<typeof SandboxBack
|
|
|
1895
2212
|
return (SANDBOX_REQUIRED_ENV[backend] ?? []).map((entry) => entry.env);
|
|
1896
2213
|
}
|
|
1897
2214
|
|
|
2215
|
+
function objectStorageConfiguredForWorkspaceArchives(settings: Settings): boolean {
|
|
2216
|
+
switch (settings.objectStorageBackend) {
|
|
2217
|
+
case "azure-blob":
|
|
2218
|
+
return Boolean(
|
|
2219
|
+
settings.objectStorageAzureConnectionString ||
|
|
2220
|
+
(settings.objectStorageAzureAccountName && settings.objectStorageAzureAccountKey),
|
|
2221
|
+
);
|
|
2222
|
+
case "gcs":
|
|
2223
|
+
return Boolean(
|
|
2224
|
+
settings.objectStorageGcsCredentialsJson ||
|
|
2225
|
+
settings.objectStorageGcsKeyFilename ||
|
|
2226
|
+
settings.objectStorageGcsProjectId,
|
|
2227
|
+
);
|
|
2228
|
+
case "aws-s3":
|
|
2229
|
+
return true;
|
|
2230
|
+
case "s3-compatible":
|
|
2231
|
+
return Boolean(
|
|
2232
|
+
settings.objectStorageEndpoint &&
|
|
2233
|
+
settings.objectStorageAccessKeyId &&
|
|
2234
|
+
settings.objectStorageSecretAccessKey,
|
|
2235
|
+
);
|
|
2236
|
+
default: {
|
|
2237
|
+
const _exhaustive: never = settings.objectStorageBackend;
|
|
2238
|
+
return _exhaustive;
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
|
|
1898
2243
|
function optional(name: string): string | undefined {
|
|
1899
2244
|
const value = process.env[name];
|
|
1900
2245
|
return value && value.trim().length > 0 ? value : undefined;
|
|
@@ -1955,6 +2300,9 @@ export function getSettings(): Settings {
|
|
|
1955
2300
|
agentStableVersion: optional("OPENGENI_AGENT_STABLE_VERSION"),
|
|
1956
2301
|
agentBetaVersion: optional("OPENGENI_AGENT_BETA_VERSION"),
|
|
1957
2302
|
productAccessMode: optional("OPENGENI_PRODUCT_ACCESS_MODE"),
|
|
2303
|
+
organizationTenancyCanonicalActivationEnabled: optional(
|
|
2304
|
+
"OPENGENI_ORGANIZATION_TENANCY_CANONICAL_ACTIVATION_ENABLED",
|
|
2305
|
+
),
|
|
1958
2306
|
billingMode: optional("OPENGENI_BILLING_MODE"),
|
|
1959
2307
|
entitlementsMode: optional("OPENGENI_ENTITLEMENTS_MODE"),
|
|
1960
2308
|
usageLimitsMode: optional("OPENGENI_USAGE_LIMITS_MODE"),
|
|
@@ -1965,7 +2313,6 @@ export function getSettings(): Settings {
|
|
|
1965
2313
|
allowedFirstPartyMcpTools: optional("OPENGENI_ALLOWED_FIRST_PARTY_MCP_TOOLS"),
|
|
1966
2314
|
streamTokenSecret: optional("OPENGENI_STREAM_TOKEN_SECRET"),
|
|
1967
2315
|
streamControlEnabled: optional("OPENGENI_STREAM_CONTROL_ENABLED"),
|
|
1968
|
-
codemodeMaxCallsPerTurn: optional("OPENGENI_CODEMODE_MAX_CALLS_PER_TURN"),
|
|
1969
2316
|
ogtoolPackageSpec: optional("OPENGENI_OGTOOL_PACKAGE_SPEC"),
|
|
1970
2317
|
environmentsEncryptionKey: optional("OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY"),
|
|
1971
2318
|
integrationsEnabled: optional("OPENGENI_INTEGRATIONS_ENABLED"),
|
|
@@ -1974,12 +2321,32 @@ export function getSettings(): Settings {
|
|
|
1974
2321
|
"OPENGENI_INTEGRATIONS_ALLOW_PRIVATE_NETWORK_TARGETS",
|
|
1975
2322
|
),
|
|
1976
2323
|
integrationsOauthClientsJson: optional("OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON"),
|
|
1977
|
-
gmailRestAdapterEnabled: optional("OPENGENI_GMAIL_REST_ADAPTER_ENABLED"),
|
|
1978
2324
|
slackClientId: optional("OPENGENI_SLACK_CLIENT_ID"),
|
|
1979
2325
|
slackClientSecret: optional("OPENGENI_SLACK_CLIENT_SECRET"),
|
|
1980
2326
|
slackSigningSecret: optional("OPENGENI_SLACK_SIGNING_SECRET"),
|
|
2327
|
+
slackBotDisplayName: optional("OPENGENI_SLACK_BOT_DISPLAY_NAME"),
|
|
2328
|
+
slackCommand: optional("OPENGENI_SLACK_COMMAND"),
|
|
1981
2329
|
googleDriveClientId: optional("OPENGENI_GOOGLE_DRIVE_CLIENT_ID"),
|
|
1982
2330
|
googleDriveClientSecret: optional("OPENGENI_GOOGLE_DRIVE_CLIENT_SECRET"),
|
|
2331
|
+
googleDriveSyncMaxItems: optional("OPENGENI_GOOGLE_DRIVE_SYNC_MAX_ITEMS"),
|
|
2332
|
+
googleDriveSyncMaxBytes: optional("OPENGENI_GOOGLE_DRIVE_SYNC_MAX_BYTES"),
|
|
2333
|
+
googleDriveSyncMaxFileBytes: optional("OPENGENI_GOOGLE_DRIVE_SYNC_MAX_FILE_BYTES"),
|
|
2334
|
+
googleDriveSyncMaxProviderRequests: optional(
|
|
2335
|
+
"OPENGENI_GOOGLE_DRIVE_SYNC_MAX_PROVIDER_REQUESTS",
|
|
2336
|
+
),
|
|
2337
|
+
googleDriveSyncMaxElapsedSeconds: optional("OPENGENI_GOOGLE_DRIVE_SYNC_MAX_ELAPSED_SECONDS"),
|
|
2338
|
+
googleDriveSyncMaxFailureDetails: optional("OPENGENI_GOOGLE_DRIVE_SYNC_MAX_FAILURE_DETAILS"),
|
|
2339
|
+
googleDriveProviderRequestTimeoutMs: optional(
|
|
2340
|
+
"OPENGENI_GOOGLE_DRIVE_PROVIDER_REQUEST_TIMEOUT_MS",
|
|
2341
|
+
),
|
|
2342
|
+
googleDriveProviderRetryAttempts: optional("OPENGENI_GOOGLE_DRIVE_PROVIDER_RETRY_ATTEMPTS"),
|
|
2343
|
+
googleDriveProviderRetryInitialDelayMs: optional(
|
|
2344
|
+
"OPENGENI_GOOGLE_DRIVE_PROVIDER_RETRY_INITIAL_DELAY_MS",
|
|
2345
|
+
),
|
|
2346
|
+
googleDriveProviderRetryMaxDelayMs: optional(
|
|
2347
|
+
"OPENGENI_GOOGLE_DRIVE_PROVIDER_RETRY_MAX_DELAY_MS",
|
|
2348
|
+
),
|
|
2349
|
+
googleDriveProviderRetryBudgetMs: optional("OPENGENI_GOOGLE_DRIVE_PROVIDER_RETRY_BUDGET_MS"),
|
|
1983
2350
|
fikenClientId: optional("OPENGENI_FIKEN_OAUTH_CLIENT_ID"),
|
|
1984
2351
|
fikenClientSecret: optional("OPENGENI_FIKEN_OAUTH_CLIENT_SECRET"),
|
|
1985
2352
|
googleDriveWorkspaceEventsEnabled: optional("OPENGENI_GOOGLE_DRIVE_WORKSPACE_EVENTS_ENABLED"),
|
|
@@ -1988,7 +2355,10 @@ export function getSettings(): Settings {
|
|
|
1988
2355
|
maxNestedAgentDepth: optional("OPENGENI_MAX_NESTED_AGENT_DEPTH"),
|
|
1989
2356
|
socialOauthClientsJson: optional("OPENGENI_SOCIAL_OAUTH_CLIENTS_JSON"),
|
|
1990
2357
|
goalMaxAutoContinuations: optional("OPENGENI_GOAL_MAX_AUTO_CONTINUATIONS"),
|
|
1991
|
-
|
|
2358
|
+
goalIdleBackoffMs: optional("OPENGENI_GOAL_IDLE_BACKOFF_MS"),
|
|
2359
|
+
goalIdleBackoffMaxMs: optional("OPENGENI_GOAL_IDLE_BACKOFF_MAX_MS"),
|
|
2360
|
+
childLifecycleNoticesEnabled: optional("OPENGENI_CHILD_LIFECYCLE_NOTICES_ENABLED"),
|
|
2361
|
+
slackWorkspaceRoutingEnabled: optional("OPENGENI_SLACK_WORKSPACE_ROUTING_ENABLED"),
|
|
1992
2362
|
agentMaxModelCallsPerTurn: optional("OPENGENI_AGENT_MAX_MODEL_CALLS_PER_TURN"),
|
|
1993
2363
|
contextWindowTokens: optional("OPENGENI_CONTEXT_WINDOW_TOKENS"),
|
|
1994
2364
|
contextEffectiveWindowTokens: optional("OPENGENI_CONTEXT_EFFECTIVE_WINDOW_TOKENS"),
|
|
@@ -2003,6 +2373,7 @@ export function getSettings(): Settings {
|
|
|
2003
2373
|
apiHost: optional("OPENGENI_API_HOST"),
|
|
2004
2374
|
apiPort: optional("OPENGENI_API_PORT"),
|
|
2005
2375
|
workerHttpPort: optional("OPENGENI_WORKER_HTTP_PORT"),
|
|
2376
|
+
opengeniMcpInternalUrl: optional("OPENGENI_MCP_INTERNAL_URL"),
|
|
2006
2377
|
opengeniMcpUrl: optional("OPENGENI_MCP_URL"),
|
|
2007
2378
|
corsAllowOriginRegex: optional("OPENGENI_CORS_ALLOW_ORIGIN_REGEX"),
|
|
2008
2379
|
openaiProvider: optional("OPENGENI_OPENAI_PROVIDER"),
|
|
@@ -2059,6 +2430,9 @@ export function getSettings(): Settings {
|
|
|
2059
2430
|
modelProvidersJson: optional("OPENGENI_MODEL_PROVIDERS_JSON"),
|
|
2060
2431
|
codexSubscriptionEnabled: optional("OPENGENI_CODEX_SUBSCRIPTION_ENABLED"),
|
|
2061
2432
|
supergrokSubscriptionEnabled: optional("OPENGENI_SUPERGROK_SUBSCRIPTION_ENABLED"),
|
|
2433
|
+
supergrokResponseStreamIdleTimeoutMs: optional(
|
|
2434
|
+
"OPENGENI_SUPERGROK_RESPONSE_STREAM_IDLE_TIMEOUT_MS",
|
|
2435
|
+
),
|
|
2062
2436
|
codexConnectedAppsEnabled: optional("OPENGENI_CODEX_CONNECTED_APPS_ENABLED"),
|
|
2063
2437
|
codexToolSearchEnabled: optional("OPENGENI_CODEX_TOOL_SEARCH_ENABLED"),
|
|
2064
2438
|
lazyToolSearchEnabled: optional("OPENGENI_LAZY_TOOL_SEARCH_ENABLED"),
|
|
@@ -2148,6 +2522,21 @@ export function getSettings(): Settings {
|
|
|
2148
2522
|
vercelProjectId: optional("OPENGENI_VERCEL_PROJECT_ID"),
|
|
2149
2523
|
vercelTeamId: optional("OPENGENI_VERCEL_TEAM_ID"),
|
|
2150
2524
|
vercelRuntime: optional("OPENGENI_VERCEL_RUNTIME"),
|
|
2525
|
+
openSandboxBaseUrl: optional("OPENGENI_OPENSANDBOX_BASE_URL"),
|
|
2526
|
+
openSandboxApiKey: optional("OPENGENI_OPENSANDBOX_API_KEY"),
|
|
2527
|
+
openSandboxImage: optional("OPENGENI_OPENSANDBOX_IMAGE"),
|
|
2528
|
+
openSandboxTtlSeconds: optional("OPENGENI_OPENSANDBOX_TTL_SECONDS"),
|
|
2529
|
+
openSandboxUseServerProxy: optional("OPENGENI_OPENSANDBOX_USE_SERVER_PROXY"),
|
|
2530
|
+
openSandboxSignedEndpoints: optional("OPENGENI_OPENSANDBOX_SIGNED_ENDPOINTS"),
|
|
2531
|
+
openSandboxSignedEndpointTtlSeconds: optional(
|
|
2532
|
+
"OPENGENI_OPENSANDBOX_SIGNED_ENDPOINT_TTL_SECONDS",
|
|
2533
|
+
),
|
|
2534
|
+
openSandboxChannelBPublicBaseUrl: optional("OPENGENI_OPENSANDBOX_CHANNEL_B_PUBLIC_BASE_URL"),
|
|
2535
|
+
openSandboxInteractionFrameProxy: optional("OPENGENI_OPENSANDBOX_INTERACTION_FRAME_PROXY"),
|
|
2536
|
+
openSandboxPoolRef: optional("OPENGENI_OPENSANDBOX_POOL_REF"),
|
|
2537
|
+
openSandboxKubernetesInventoryNamespace: optional(
|
|
2538
|
+
"OPENGENI_OPENSANDBOX_KUBERNETES_INVENTORY_NAMESPACE",
|
|
2539
|
+
),
|
|
2151
2540
|
sandboxOwnershipEnabled: optional("OPENGENI_SANDBOX_OWNERSHIP_ENABLED"),
|
|
2152
2541
|
rigVerificationLeaseOwnershipEnabled: optional(
|
|
2153
2542
|
"OPENGENI_RIG_VERIFICATION_LEASE_OWNERSHIP_ENABLED",
|
|
@@ -2179,6 +2568,7 @@ export function getSettings(): Settings {
|
|
|
2179
2568
|
sandboxLeaseTtlMs: optional("OPENGENI_SANDBOX_LEASE_TTL_MS"),
|
|
2180
2569
|
sandboxLeaseWarmingTtlMs: optional("OPENGENI_SANDBOX_LEASE_WARMING_TTL_MS"),
|
|
2181
2570
|
sandboxWarmingTimeoutMs: optional("OPENGENI_SANDBOX_WARMING_TIMEOUT_MS"),
|
|
2571
|
+
workspaceControlLockTimeoutMs: optional("OPENGENI_WORKSPACE_CONTROL_LOCK_TIMEOUT_MS"),
|
|
2182
2572
|
rigSetupTimeoutMs: optional("OPENGENI_RIG_SETUP_TIMEOUT_MS"),
|
|
2183
2573
|
sandboxWarmRateMicrosPerSecondJson: optional(
|
|
2184
2574
|
"OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON",
|
|
@@ -2225,6 +2615,10 @@ export function getSettings(): Settings {
|
|
|
2225
2615
|
githubAppId: optional("OPENGENI_GITHUB_APP_ID"),
|
|
2226
2616
|
githubClientId: optional("OPENGENI_GITHUB_CLIENT_ID"),
|
|
2227
2617
|
githubClientSecret: optional("OPENGENI_GITHUB_CLIENT_SECRET"),
|
|
2618
|
+
githubRestMcpEnabled: optional("OPENGENI_GITHUB_REST_MCP_ENABLED"),
|
|
2619
|
+
githubPersonalOauthEnabled: optional("OPENGENI_GITHUB_PERSONAL_OAUTH_ENABLED"),
|
|
2620
|
+
githubPersonalOauthClientId: optional("OPENGENI_GITHUB_PERSONAL_OAUTH_CLIENT_ID"),
|
|
2621
|
+
githubPersonalOauthClientSecret: optional("OPENGENI_GITHUB_PERSONAL_OAUTH_CLIENT_SECRET"),
|
|
2228
2622
|
githubAppSlug: optional("OPENGENI_GITHUB_APP_SLUG"),
|
|
2229
2623
|
githubWebhookSecret: optional("OPENGENI_GITHUB_WEBHOOK_SECRET"),
|
|
2230
2624
|
githubAppPrivateKey: optional("OPENGENI_GITHUB_APP_PRIVATE_KEY"),
|
|
@@ -2244,11 +2638,11 @@ export function getSettings(): Settings {
|
|
|
2244
2638
|
const settings = {
|
|
2245
2639
|
...parsed,
|
|
2246
2640
|
sandboxIdleGraceMs:
|
|
2247
|
-
raw.sandboxIdleGraceMs === undefined
|
|
2641
|
+
raw.sandboxIdleGraceMs === undefined && parsed.sandboxBackend === "modal"
|
|
2248
2642
|
? Math.min(900_000, Math.floor((parsed.modalTimeoutSeconds * 1000) / 2))
|
|
2249
2643
|
: parsed.sandboxIdleGraceMs,
|
|
2250
2644
|
sandboxRotationLeadMs:
|
|
2251
|
-
raw.sandboxRotationLeadMs === undefined
|
|
2645
|
+
raw.sandboxRotationLeadMs === undefined && parsed.sandboxBackend === "modal"
|
|
2252
2646
|
? Math.min(3_600_000, Math.floor((parsed.modalTimeoutSeconds * 1000) / 2))
|
|
2253
2647
|
: parsed.sandboxRotationLeadMs,
|
|
2254
2648
|
mcpServers: ensureBuiltInMcpServers(parsed),
|
|
@@ -2321,6 +2715,45 @@ export function effectiveModalIdleTimeoutSeconds(settings: Settings): number {
|
|
|
2321
2715
|
return settings.modalIdleTimeoutSeconds ?? settings.modalTimeoutSeconds;
|
|
2322
2716
|
}
|
|
2323
2717
|
|
|
2718
|
+
export type EffectiveSandboxLifecycle = {
|
|
2719
|
+
hardLifetimeMs: number | null;
|
|
2720
|
+
renewableTtlSeconds: number | null;
|
|
2721
|
+
providerIdleTimeoutMs: number | null;
|
|
2722
|
+
rotationLeadMs: number | null;
|
|
2723
|
+
};
|
|
2724
|
+
|
|
2725
|
+
/** Resolve provider lifecycle clocks without teaching generic callers Modal or
|
|
2726
|
+
* OpenSandbox field names. Modal's returned values are exactly the pre-existing
|
|
2727
|
+
* hard/idle/rotation values; OpenSandbox instead exposes a renewable TTL and no
|
|
2728
|
+
* finite-deadline rotation. */
|
|
2729
|
+
export function effectiveSandboxLifecycle(
|
|
2730
|
+
settings: Settings,
|
|
2731
|
+
backend: z.infer<typeof SandboxBackend> = settings.sandboxBackend,
|
|
2732
|
+
): EffectiveSandboxLifecycle {
|
|
2733
|
+
if (backend === "modal") {
|
|
2734
|
+
return {
|
|
2735
|
+
hardLifetimeMs: settings.modalTimeoutSeconds * 1000,
|
|
2736
|
+
renewableTtlSeconds: null,
|
|
2737
|
+
providerIdleTimeoutMs: effectiveModalIdleTimeoutSeconds(settings) * 1000,
|
|
2738
|
+
rotationLeadMs: settings.sandboxRotationLeadMs,
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
if (backend === "opensandbox") {
|
|
2742
|
+
return {
|
|
2743
|
+
hardLifetimeMs: null,
|
|
2744
|
+
renewableTtlSeconds: settings.openSandboxTtlSeconds,
|
|
2745
|
+
providerIdleTimeoutMs: null,
|
|
2746
|
+
rotationLeadMs: null,
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
return {
|
|
2750
|
+
hardLifetimeMs: CAPABILITY_DESCRIPTORS[backend].lifetime.hardLifetimeMs ?? null,
|
|
2751
|
+
renewableTtlSeconds: null,
|
|
2752
|
+
providerIdleTimeoutMs: null,
|
|
2753
|
+
rotationLeadMs: null,
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2324
2757
|
/**
|
|
2325
2758
|
* One shared upper bound for the durable provider-capture claim and for command
|
|
2326
2759
|
* admission waiting behind it. The SDK request itself is bounded by
|
|
@@ -2733,6 +3166,7 @@ function gatewayRegistryProvider(
|
|
|
2733
3166
|
// Model-specific compatibility stays at the reviewed request fence rather
|
|
2734
3167
|
// than downgrading the whole provider wire.
|
|
2735
3168
|
api: "responses",
|
|
3169
|
+
wireProfile: "openai",
|
|
2736
3170
|
baseUrl: VERCEL_AI_GATEWAY_BASE_URL,
|
|
2737
3171
|
...(input.apiKey ? { apiKey: input.apiKey } : {}),
|
|
2738
3172
|
models,
|
|
@@ -2841,18 +3275,37 @@ export function productShortLabelForModelId(modelId: string): string | null {
|
|
|
2841
3275
|
}
|
|
2842
3276
|
}
|
|
2843
3277
|
|
|
3278
|
+
const BUILTIN_GPT56_MODEL_IDS = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] as const;
|
|
3279
|
+
|
|
3280
|
+
function isBuiltinGpt56ModelId(modelId: string): boolean {
|
|
3281
|
+
return (BUILTIN_GPT56_MODEL_IDS as readonly string[]).includes(modelId);
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
/** Billed GPT-5.6 uses the same raw/effective/auto-compact catalog as Codex. */
|
|
3285
|
+
function builtinContextLimitsForModel(
|
|
3286
|
+
settings: Settings,
|
|
3287
|
+
modelId: string,
|
|
3288
|
+
): Pick<
|
|
3289
|
+
ConfiguredModel,
|
|
3290
|
+
"contextWindowTokens" | "effectiveContextWindowTokens" | "autoCompactTokenLimit"
|
|
3291
|
+
> {
|
|
3292
|
+
if (isBuiltinGpt56ModelId(modelId)) {
|
|
3293
|
+
return {
|
|
3294
|
+
contextWindowTokens: CODEX_MODEL_CONTEXT_WINDOW_TOKENS,
|
|
3295
|
+
effectiveContextWindowTokens: CODEX_MODEL_EFFECTIVE_CONTEXT_WINDOW_TOKENS,
|
|
3296
|
+
autoCompactTokenLimit: CODEX_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
|
|
3297
|
+
};
|
|
3298
|
+
}
|
|
3299
|
+
return { contextWindowTokens: settings.contextWindowTokens };
|
|
3300
|
+
}
|
|
3301
|
+
|
|
2844
3302
|
function builtinLatencyModesForModel(modelId: string): Array<{
|
|
2845
3303
|
id: z.infer<typeof ModelLatencyModeV1>;
|
|
2846
3304
|
upstream: "supported" | "unsupported" | "unknown";
|
|
2847
3305
|
runnable: boolean;
|
|
2848
3306
|
billingMultiplierBps?: number;
|
|
2849
3307
|
}> {
|
|
2850
|
-
if (
|
|
2851
|
-
modelId === "gpt-5.6-sol" ||
|
|
2852
|
-
modelId === "gpt-5.6-terra" ||
|
|
2853
|
-
modelId === "gpt-5.6-luna" ||
|
|
2854
|
-
modelId.startsWith("codex/gpt-5.6-")
|
|
2855
|
-
) {
|
|
3308
|
+
if (isBuiltinGpt56ModelId(modelId) || modelId.startsWith("codex/gpt-5.6-")) {
|
|
2856
3309
|
return [
|
|
2857
3310
|
{ id: "standard", upstream: "supported", runnable: true },
|
|
2858
3311
|
{
|
|
@@ -2882,7 +3335,7 @@ function builtinHostedImageGenerationForModel(settings: Settings, modelId: strin
|
|
|
2882
3335
|
return (
|
|
2883
3336
|
settings.openaiProvider === "openai" &&
|
|
2884
3337
|
isDirectOpenAiApiBaseUrl(settings.openaiBaseUrl) &&
|
|
2885
|
-
|
|
3338
|
+
isBuiltinGpt56ModelId(modelId)
|
|
2886
3339
|
);
|
|
2887
3340
|
}
|
|
2888
3341
|
|
|
@@ -2966,6 +3419,9 @@ function assertLatencyModeRunnable(
|
|
|
2966
3419
|
}
|
|
2967
3420
|
|
|
2968
3421
|
function registryCredentialSource(provider: RegistryProvider): CredentialSourceV1 {
|
|
3422
|
+
if (provider.kind === "anonymous") {
|
|
3423
|
+
return { kind: "deployment", mechanism: "none" };
|
|
3424
|
+
}
|
|
2969
3425
|
if (provider.kind === "codex-subscription") {
|
|
2970
3426
|
return { kind: "connected_subscription", provider: "codex" };
|
|
2971
3427
|
}
|
|
@@ -2979,6 +3435,9 @@ function registryCredentialSource(provider: RegistryProvider): CredentialSourceV
|
|
|
2979
3435
|
}
|
|
2980
3436
|
|
|
2981
3437
|
function registryBilling(provider: RegistryProvider): BillingAttributionV1 {
|
|
3438
|
+
if (provider.kind === "anonymous") {
|
|
3439
|
+
return { upstreamPayer: "deployment", metering: "external" };
|
|
3440
|
+
}
|
|
2982
3441
|
if (provider.kind === "codex-subscription" || provider.kind === "xai-subscription") {
|
|
2983
3442
|
return { upstreamPayer: "connected_subscription", metering: "external" };
|
|
2984
3443
|
}
|
|
@@ -3060,6 +3519,7 @@ function definitionVersionFor(
|
|
|
3060
3519
|
provider: {
|
|
3061
3520
|
adapterKind: provider.kind,
|
|
3062
3521
|
wireApi: provider.api,
|
|
3522
|
+
wireProfile: provider.wireProfile,
|
|
3063
3523
|
baseUrl: provider.baseUrl ?? null,
|
|
3064
3524
|
defaultHeaders: requestMetadata.headers,
|
|
3065
3525
|
defaultQuery: requestMetadata.query,
|
|
@@ -3107,6 +3567,7 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
|
|
|
3107
3567
|
label: builtinProviderLabel(settings),
|
|
3108
3568
|
kind: "api-key",
|
|
3109
3569
|
api: "responses",
|
|
3570
|
+
wireProfile: settings.openaiProvider === "azure" ? "azure-openai" : "openai",
|
|
3110
3571
|
builtin: true,
|
|
3111
3572
|
credentialSource,
|
|
3112
3573
|
billing: { upstreamPayer: "deployment", metering: "opengeni_credits" },
|
|
@@ -3127,6 +3588,7 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
|
|
|
3127
3588
|
label: provider.label ?? provider.id,
|
|
3128
3589
|
kind: provider.kind,
|
|
3129
3590
|
api: provider.api,
|
|
3591
|
+
wireProfile: provider.wireProfile,
|
|
3130
3592
|
builtin: false,
|
|
3131
3593
|
baseUrl: provider.baseUrl,
|
|
3132
3594
|
apiKey: resolveProviderApiKey(provider),
|
|
@@ -3157,6 +3619,7 @@ export function withCodexCatalogProvider(settings: Settings): Settings {
|
|
|
3157
3619
|
id: CODEX_PROVIDER_ID,
|
|
3158
3620
|
label: "Codex (ChatGPT subscription)",
|
|
3159
3621
|
api: "responses",
|
|
3622
|
+
wireProfile: "openai",
|
|
3160
3623
|
baseUrl: CODEX_PROVIDER_BASE_URL,
|
|
3161
3624
|
models: CODEX_FALLBACK_MODEL_SLUGS.map((slug) => {
|
|
3162
3625
|
const capabilities = {
|
|
@@ -3214,11 +3677,13 @@ export function withXaiSubscriptionCatalogProvider(settings: Settings): Settings
|
|
|
3214
3677
|
id: XAI_SUBSCRIPTION_PROVIDER_ID,
|
|
3215
3678
|
label: "SuperGrok (xAI subscription)",
|
|
3216
3679
|
api: "responses",
|
|
3680
|
+
wireProfile: "openai",
|
|
3217
3681
|
baseUrl: XAI_SUBSCRIPTION_PROXY_BASE_URL,
|
|
3218
3682
|
models: XAI_SUBSCRIPTION_MODEL_SLUGS.map((slug) => {
|
|
3219
3683
|
const capabilities = legacyModelCapabilities(settings, {
|
|
3220
3684
|
reasoningEffort: true,
|
|
3221
3685
|
hostedWebSearch: true,
|
|
3686
|
+
vision: true,
|
|
3222
3687
|
});
|
|
3223
3688
|
capabilities.reasoning.efforts = ["low", "medium", "high", "xhigh"];
|
|
3224
3689
|
capabilities.reasoning.defaultEffort = "high";
|
|
@@ -3430,7 +3895,7 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
|
|
|
3430
3895
|
billing: builtinProvider.billing,
|
|
3431
3896
|
capabilities,
|
|
3432
3897
|
...(pricingSchedules[id] === undefined ? {} : { pricing: pricingSchedules[id] }),
|
|
3433
|
-
|
|
3898
|
+
...builtinContextLimitsForModel(settings, id),
|
|
3434
3899
|
toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
|
|
3435
3900
|
reasoningEffort: capabilities.reasoning.runnable,
|
|
3436
3901
|
hostedWebSearch: capabilities.hostedTools.webSearch.runnable,
|
|
@@ -4678,18 +5143,11 @@ function ensureBuiltInMcpServers(settings: Settings): Settings["mcpServers"] {
|
|
|
4678
5143
|
}
|
|
4679
5144
|
|
|
4680
5145
|
/**
|
|
4681
|
-
* The base URL of OpenGeni's
|
|
4682
|
-
* template
|
|
4683
|
-
* decision. Every site that needs the first-party MCP base (config's tool
|
|
4684
|
-
* registry here, and the worker-side `firstPartyMcpServerUrlForRun` /
|
|
4685
|
-
* `firstPartyMcpUrls` in @opengeni/runtime) MUST route through this so the
|
|
4686
|
-
* default lives in exactly one place.
|
|
5146
|
+
* The sandbox/external base URL of OpenGeni's first-party MCP endpoint, as a
|
|
5147
|
+
* `{workspaceId}` template. Codemode and remote placements use this route.
|
|
4687
5148
|
*
|
|
4688
5149
|
* BINDING CONTRACT (`opengeniMcpUrl`):
|
|
4689
|
-
* - STANDALONE (unset): falls back to the loopback default
|
|
4690
|
-
* `http://127.0.0.1:${apiPort}/v1/workspaces/{workspaceId}/mcp` — the worker
|
|
4691
|
-
* and API are in/next to the same host:port, so loopback resolves the
|
|
4692
|
-
* workspace-scoped MCP. Byte-for-byte today's behavior.
|
|
5150
|
+
* - STANDALONE (unset): falls back to the loopback default.
|
|
4693
5151
|
* - EMBEDDED / MOUNTED (must set): when OpenGeni's API is mounted as a host
|
|
4694
5152
|
* sub-app under a prefix (e.g. `https://host/og/v1/...`), the loopback
|
|
4695
5153
|
* default is WRONG — the worker runs in the host process and `127.0.0.1:
|
|
@@ -4705,8 +5163,19 @@ export function firstPartyMcpBaseUrl(settings: Settings): string {
|
|
|
4705
5163
|
);
|
|
4706
5164
|
}
|
|
4707
5165
|
|
|
4708
|
-
|
|
4709
|
-
|
|
5166
|
+
/**
|
|
5167
|
+
* Worker-side first-party MCP base. It never inherits the public/tunnel URL:
|
|
5168
|
+
* an operator may intentionally expose that route to Modal or a Connected
|
|
5169
|
+
* Machine, while the worker should still use loopback or cluster service DNS.
|
|
5170
|
+
*/
|
|
5171
|
+
export function firstPartyMcpInternalBaseUrl(settings: Settings): string {
|
|
5172
|
+
return (
|
|
5173
|
+
settings.opengeniMcpInternalUrl ??
|
|
5174
|
+
`http://127.0.0.1:${settings.apiPort}/v1/workspaces/{workspaceId}/mcp`
|
|
5175
|
+
);
|
|
5176
|
+
}
|
|
5177
|
+
|
|
5178
|
+
function scopedFirstPartyMcpUrl(raw: string, workspaceId: string): string {
|
|
4710
5179
|
if (raw.includes("{workspaceId}")) {
|
|
4711
5180
|
return raw.replaceAll("{workspaceId}", workspaceId);
|
|
4712
5181
|
}
|
|
@@ -4717,6 +5186,14 @@ export function firstPartyMcpWorkspaceUrl(settings: Settings, workspaceId: strin
|
|
|
4717
5186
|
return url.toString();
|
|
4718
5187
|
}
|
|
4719
5188
|
|
|
5189
|
+
export function firstPartyMcpWorkspaceUrl(settings: Settings, workspaceId: string): string {
|
|
5190
|
+
return scopedFirstPartyMcpUrl(firstPartyMcpBaseUrl(settings), workspaceId);
|
|
5191
|
+
}
|
|
5192
|
+
|
|
5193
|
+
export function firstPartyMcpInternalWorkspaceUrl(settings: Settings, workspaceId: string): string {
|
|
5194
|
+
return scopedFirstPartyMcpUrl(firstPartyMcpInternalBaseUrl(settings), workspaceId);
|
|
5195
|
+
}
|
|
5196
|
+
|
|
4720
5197
|
export function codemodeWorkspaceUrl(settings: Settings, workspaceId: string): string {
|
|
4721
5198
|
if (settings.opengeniMcpUrl) {
|
|
4722
5199
|
const url = new URL(firstPartyMcpWorkspaceUrl(settings, workspaceId));
|
|
@@ -4757,8 +5234,23 @@ function firstPartyFilesMcpServerUrl(mcpUrl: string): string {
|
|
|
4757
5234
|
return `${mcpUrl.replace(/\/+$/, "")}/files`;
|
|
4758
5235
|
}
|
|
4759
5236
|
|
|
5237
|
+
const MODAL_DESKTOP_IMAGE_DIGEST_REF = /@sha256:[0-9a-f]{64}$/i;
|
|
5238
|
+
|
|
5239
|
+
function isDigestPinnedModalDesktopImage(settings: Settings): boolean {
|
|
5240
|
+
if (settings.modalImageId) return true;
|
|
5241
|
+
return (
|
|
5242
|
+
typeof settings.modalImageRef === "string" &&
|
|
5243
|
+
MODAL_DESKTOP_IMAGE_DIGEST_REF.test(settings.modalImageRef)
|
|
5244
|
+
);
|
|
5245
|
+
}
|
|
5246
|
+
|
|
4760
5247
|
function validateSettings(settings: Settings): void {
|
|
4761
5248
|
temporalConnectionOptions(settings);
|
|
5249
|
+
if (settings.goalIdleBackoffMs.some((delayMs) => delayMs > settings.goalIdleBackoffMaxMs)) {
|
|
5250
|
+
throw new Error(
|
|
5251
|
+
`OPENGENI_GOAL_IDLE_BACKOFF_MS entries must not exceed OPENGENI_GOAL_IDLE_BACKOFF_MAX_MS (${settings.goalIdleBackoffMaxMs})`,
|
|
5252
|
+
);
|
|
5253
|
+
}
|
|
4762
5254
|
const allowedFirstPartyMcpTools = new Set(
|
|
4763
5255
|
settings.allowedFirstPartyMcpTools ?? FIRST_PARTY_MCP_TOOL_NAMES,
|
|
4764
5256
|
);
|
|
@@ -4847,6 +5339,59 @@ function validateSettings(settings: Settings): void {
|
|
|
4847
5339
|
"OPENGENI_GOOGLE_DRIVE_CLIENT_ID and OPENGENI_GOOGLE_DRIVE_CLIENT_SECRET must be configured together",
|
|
4848
5340
|
);
|
|
4849
5341
|
}
|
|
5342
|
+
if (
|
|
5343
|
+
Boolean(settings.githubPersonalOauthClientId) !==
|
|
5344
|
+
Boolean(settings.githubPersonalOauthClientSecret)
|
|
5345
|
+
) {
|
|
5346
|
+
throw new Error(
|
|
5347
|
+
"OPENGENI_GITHUB_PERSONAL_OAUTH_CLIENT_ID and OPENGENI_GITHUB_PERSONAL_OAUTH_CLIENT_SECRET must be configured together",
|
|
5348
|
+
);
|
|
5349
|
+
}
|
|
5350
|
+
if (settings.githubPersonalOauthEnabled) {
|
|
5351
|
+
if (!settings.integrationsEnabled) {
|
|
5352
|
+
throw new Error(
|
|
5353
|
+
"OPENGENI_INTEGRATIONS_ENABLED=true is required when personal GitHub OAuth is enabled",
|
|
5354
|
+
);
|
|
5355
|
+
}
|
|
5356
|
+
if (settings.productAccessMode !== "managed") {
|
|
5357
|
+
throw new Error(
|
|
5358
|
+
"OPENGENI_GITHUB_PERSONAL_OAUTH_ENABLED=true requires OPENGENI_PRODUCT_ACCESS_MODE=managed",
|
|
5359
|
+
);
|
|
5360
|
+
}
|
|
5361
|
+
if (!settings.githubPersonalOauthClientId || !settings.githubPersonalOauthClientSecret) {
|
|
5362
|
+
throw new Error(
|
|
5363
|
+
"personal GitHub OAuth requires OPENGENI_GITHUB_PERSONAL_OAUTH_CLIENT_ID and OPENGENI_GITHUB_PERSONAL_OAUTH_CLIENT_SECRET",
|
|
5364
|
+
);
|
|
5365
|
+
}
|
|
5366
|
+
if (settings.githubPersonalOauthClientId === settings.githubClientId) {
|
|
5367
|
+
throw new Error(
|
|
5368
|
+
"personal GitHub OAuth must use a different OAuth App client from the OpenGeni GitHub App",
|
|
5369
|
+
);
|
|
5370
|
+
}
|
|
5371
|
+
if (!personalGitHubOAuthCallbackUrl(settings.publicBaseUrl)) {
|
|
5372
|
+
throw new Error(
|
|
5373
|
+
"OPENGENI_PUBLIC_BASE_URL must be a credential-free origin without a path, query, or fragment when personal GitHub OAuth is enabled",
|
|
5374
|
+
);
|
|
5375
|
+
}
|
|
5376
|
+
if (
|
|
5377
|
+
!settings.publicBaseUrl?.startsWith("https://") &&
|
|
5378
|
+
!["local", "test"].includes(settings.environment)
|
|
5379
|
+
) {
|
|
5380
|
+
throw new Error(
|
|
5381
|
+
"OPENGENI_PUBLIC_BASE_URL must use https when personal GitHub OAuth is enabled outside local/test",
|
|
5382
|
+
);
|
|
5383
|
+
}
|
|
5384
|
+
if (!settings.integrationsStateSecret) {
|
|
5385
|
+
throw new Error(
|
|
5386
|
+
"OPENGENI_INTEGRATIONS_STATE_SECRET is required when personal GitHub OAuth is enabled",
|
|
5387
|
+
);
|
|
5388
|
+
}
|
|
5389
|
+
if (!settings.environmentsEncryptionKey) {
|
|
5390
|
+
throw new Error(
|
|
5391
|
+
"OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is required when personal GitHub OAuth is enabled",
|
|
5392
|
+
);
|
|
5393
|
+
}
|
|
5394
|
+
}
|
|
4850
5395
|
if (Boolean(settings.fikenClientId) !== Boolean(settings.fikenClientSecret)) {
|
|
4851
5396
|
throw new Error(
|
|
4852
5397
|
"OPENGENI_FIKEN_OAUTH_CLIENT_ID and OPENGENI_FIKEN_OAUTH_CLIENT_SECRET must be configured together",
|
|
@@ -4873,11 +5418,21 @@ function validateSettings(settings: Settings): void {
|
|
|
4873
5418
|
}
|
|
4874
5419
|
}
|
|
4875
5420
|
if (settings.googleDriveClientId) {
|
|
5421
|
+
if (!settings.integrationsEnabled) {
|
|
5422
|
+
throw new Error(
|
|
5423
|
+
"OPENGENI_INTEGRATIONS_ENABLED=true is required when the Google Drive integration is configured",
|
|
5424
|
+
);
|
|
5425
|
+
}
|
|
4876
5426
|
if (!settings.publicBaseUrl) {
|
|
4877
5427
|
throw new Error(
|
|
4878
5428
|
"OPENGENI_PUBLIC_BASE_URL is required when the Google Drive integration is configured",
|
|
4879
5429
|
);
|
|
4880
5430
|
}
|
|
5431
|
+
if (!googleDriveOAuthCallbackUrl(settings.publicBaseUrl)) {
|
|
5432
|
+
throw new Error(
|
|
5433
|
+
"OPENGENI_PUBLIC_BASE_URL must be a credential-free origin without a path, query, or fragment when the Google Drive integration is configured",
|
|
5434
|
+
);
|
|
5435
|
+
}
|
|
4881
5436
|
if (
|
|
4882
5437
|
!settings.publicBaseUrl.startsWith("https://") &&
|
|
4883
5438
|
!["local", "test"].includes(settings.environment)
|
|
@@ -4892,6 +5447,23 @@ function validateSettings(settings: Settings): void {
|
|
|
4892
5447
|
);
|
|
4893
5448
|
}
|
|
4894
5449
|
}
|
|
5450
|
+
if (settings.googleDriveSyncMaxFileBytes > settings.googleDriveSyncMaxBytes) {
|
|
5451
|
+
throw new Error(
|
|
5452
|
+
"OPENGENI_GOOGLE_DRIVE_SYNC_MAX_FILE_BYTES must not exceed OPENGENI_GOOGLE_DRIVE_SYNC_MAX_BYTES",
|
|
5453
|
+
);
|
|
5454
|
+
}
|
|
5455
|
+
if (
|
|
5456
|
+
settings.googleDriveProviderRetryInitialDelayMs > settings.googleDriveProviderRetryMaxDelayMs
|
|
5457
|
+
) {
|
|
5458
|
+
throw new Error(
|
|
5459
|
+
"OPENGENI_GOOGLE_DRIVE_PROVIDER_RETRY_INITIAL_DELAY_MS must not exceed OPENGENI_GOOGLE_DRIVE_PROVIDER_RETRY_MAX_DELAY_MS",
|
|
5460
|
+
);
|
|
5461
|
+
}
|
|
5462
|
+
if (settings.googleDriveProviderRetryInitialDelayMs > settings.googleDriveProviderRetryBudgetMs) {
|
|
5463
|
+
throw new Error(
|
|
5464
|
+
"OPENGENI_GOOGLE_DRIVE_PROVIDER_RETRY_INITIAL_DELAY_MS must not exceed OPENGENI_GOOGLE_DRIVE_PROVIDER_RETRY_BUDGET_MS",
|
|
5465
|
+
);
|
|
5466
|
+
}
|
|
4895
5467
|
if (Boolean(settings.atlassianClientId) !== Boolean(settings.atlassianClientSecret)) {
|
|
4896
5468
|
throw new Error(
|
|
4897
5469
|
"OPENGENI_ATLASSIAN_CLIENT_ID and OPENGENI_ATLASSIAN_CLIENT_SECRET must be configured together",
|
|
@@ -5120,6 +5692,18 @@ function validateSettings(settings: Settings): void {
|
|
|
5120
5692
|
sandboxLifecycleHookIds(settings);
|
|
5121
5693
|
// Fail fast on a malformed warm-rate table (P2.1).
|
|
5122
5694
|
parseSandboxWarmRateJson(settings.sandboxWarmRateMicrosPerSecondJson);
|
|
5695
|
+
if (settings.sandboxBackend === "opensandbox") {
|
|
5696
|
+
if (!/@sha256:[0-9a-f]{64}$/i.test(settings.openSandboxImage ?? "")) {
|
|
5697
|
+
throw new Error(
|
|
5698
|
+
"OPENGENI_OPENSANDBOX_IMAGE must be an immutable OCI reference ending in @sha256:<64 hex characters>",
|
|
5699
|
+
);
|
|
5700
|
+
}
|
|
5701
|
+
if (!objectStorageConfiguredForWorkspaceArchives(settings)) {
|
|
5702
|
+
throw new Error(
|
|
5703
|
+
"OPENGENI_SANDBOX_BACKEND=opensandbox requires configured object storage for portable /workspace archives",
|
|
5704
|
+
);
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5123
5707
|
const serverIds = new Set<string>();
|
|
5124
5708
|
for (const server of settings.mcpServers) {
|
|
5125
5709
|
if (serverIds.has(server.id)) {
|
|
@@ -5128,30 +5712,13 @@ function validateSettings(settings: Settings): void {
|
|
|
5128
5712
|
serverIds.add(server.id);
|
|
5129
5713
|
}
|
|
5130
5714
|
// --- sandbox lease cadence invariant (fail fast at boot) ---
|
|
5131
|
-
//
|
|
5132
|
-
//
|
|
5133
|
-
//
|
|
5134
|
-
// - the reaper must terminate a genuinely-idle box (after the full drain grace,
|
|
5135
|
-
// observed on the NEXT sweep) BEFORE the provider's hard lifetime reclaims it
|
|
5136
|
-
// out from under us — the provider lifetime is the backstop, not the
|
|
5137
|
-
// warm-window controller. idleGrace counts from the user's last release;
|
|
5138
|
-
// the provider clock counts from the preceding resume, so we leave the
|
|
5139
|
-
// active-turn headroom in modalTimeoutSeconds (default 86400s).
|
|
5715
|
+
// Holder TTLs are provider-neutral. Modal's finite hard/idle clocks and
|
|
5716
|
+
// deadline rotation are validated only when Modal is active; renewable-TTL
|
|
5717
|
+
// providers such as OpenSandbox do not enter that deadline model.
|
|
5140
5718
|
{
|
|
5141
5719
|
const reaperPeriod = settings.sandboxLeaseReaperPeriodMs;
|
|
5142
5720
|
const viewerTtl = settings.sandboxViewerHolderTtlMs;
|
|
5143
5721
|
const interactionTtl = settings.sandboxInteractionHolderTtlMs;
|
|
5144
|
-
const idleGraceMs = settings.sandboxIdleGraceMs;
|
|
5145
|
-
const providerLifetimeMs = settings.modalTimeoutSeconds * 1000;
|
|
5146
|
-
const rotationLeadMs = settings.sandboxRotationLeadMs;
|
|
5147
|
-
// The EFFECTIVE box lifetime when it sits idle between turns is the Modal IDLE
|
|
5148
|
-
// timeout, NOT the hard lifetime (sandbox-file-persistence): a box with no
|
|
5149
|
-
// active connection is idle-reaped at idleTimeout. effectiveModalIdleTimeout
|
|
5150
|
-
// defaults to the hard lifetime (so the idle-reap never beats the OpenGeni
|
|
5151
|
-
// reaper), but an operator can pin it shorter — the invariants below bind the
|
|
5152
|
-
// reaper cadence + drain grace to the idle timeout (the REAL ceiling), so a
|
|
5153
|
-
// drained box always survives long enough for the reaper to snapshot it.
|
|
5154
|
-
const idleTimeoutMs = effectiveModalIdleTimeoutSeconds(settings) * 1000;
|
|
5155
5722
|
if (!(reaperPeriod < viewerTtl)) {
|
|
5156
5723
|
throw new Error(
|
|
5157
5724
|
`OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS (${reaperPeriod}) must be strictly less than ` +
|
|
@@ -5166,54 +5733,57 @@ function validateSettings(settings: Settings): void {
|
|
|
5166
5733
|
`more often than the controller-heartbeat horizon.`,
|
|
5167
5734
|
);
|
|
5168
5735
|
}
|
|
5169
|
-
if (
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
`
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
`
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
`
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5736
|
+
if (settings.sandboxBackend === "modal") {
|
|
5737
|
+
const idleGraceMs = settings.sandboxIdleGraceMs;
|
|
5738
|
+
const lifecycle = effectiveSandboxLifecycle(settings, "modal");
|
|
5739
|
+
const providerLifetimeMs = lifecycle.hardLifetimeMs!;
|
|
5740
|
+
const rotationLeadMs = lifecycle.rotationLeadMs!;
|
|
5741
|
+
const idleTimeoutMs = lifecycle.providerIdleTimeoutMs!;
|
|
5742
|
+
if (!(idleTimeoutMs <= providerLifetimeMs)) {
|
|
5743
|
+
throw new Error(
|
|
5744
|
+
`OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS*1000 (${idleTimeoutMs}) must not exceed the hard provider ` +
|
|
5745
|
+
`lifetime (OPENGENI_MODAL_TIMEOUT_SECONDS*1000 = ${providerLifetimeMs}): the idle timeout is a ` +
|
|
5746
|
+
`floor under the hard lifetime, not above it.`,
|
|
5747
|
+
);
|
|
5748
|
+
}
|
|
5749
|
+
if (!(rotationLeadMs < providerLifetimeMs)) {
|
|
5750
|
+
throw new Error(
|
|
5751
|
+
`OPENGENI_SANDBOX_ROTATION_LEAD_MS (${rotationLeadMs}) must be strictly less than ` +
|
|
5752
|
+
`OPENGENI_MODAL_TIMEOUT_SECONDS*1000 (${providerLifetimeMs}).`,
|
|
5753
|
+
);
|
|
5754
|
+
}
|
|
5755
|
+
const captureTimeoutMs = sandboxArchiveCaptureTimeoutMs(settings);
|
|
5756
|
+
if (!(rotationLeadMs > captureTimeoutMs + reaperPeriod)) {
|
|
5757
|
+
throw new Error(
|
|
5758
|
+
`OPENGENI_SANDBOX_ROTATION_LEAD_MS (${rotationLeadMs}) must exceed the durable capture ` +
|
|
5759
|
+
`timeout plus one reaper period (${captureTimeoutMs + reaperPeriod}).`,
|
|
5760
|
+
);
|
|
5761
|
+
}
|
|
5762
|
+
if (!(viewerTtl < idleTimeoutMs)) {
|
|
5763
|
+
throw new Error(
|
|
5764
|
+
`OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS (${viewerTtl}) must be strictly less than the effective box ` +
|
|
5765
|
+
`idle timeout (${idleTimeoutMs}): a viewer holder must be reapable before the box idles out from ` +
|
|
5766
|
+
`under it (the provider idle-timeout is the backstop).`,
|
|
5767
|
+
);
|
|
5768
|
+
}
|
|
5769
|
+
if (!(interactionTtl < idleTimeoutMs)) {
|
|
5770
|
+
throw new Error(
|
|
5771
|
+
`OPENGENI_SANDBOX_INTERACTION_HOLDER_TTL_MS (${interactionTtl}) must be strictly less than ` +
|
|
5772
|
+
`the effective box idle timeout (${idleTimeoutMs}): a dead browser controller must be ` +
|
|
5773
|
+
`reapable before the provider reclaims its placement.`,
|
|
5774
|
+
);
|
|
5775
|
+
}
|
|
5776
|
+
if (!(reaperPeriod + idleGraceMs < idleTimeoutMs)) {
|
|
5777
|
+
throw new Error(
|
|
5778
|
+
`OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS + OPENGENI_SANDBOX_IDLE_GRACE_MS ` +
|
|
5779
|
+
`(${reaperPeriod} + ${idleGraceMs} = ${reaperPeriod + idleGraceMs}) must be strictly less than the ` +
|
|
5780
|
+
`effective box idle timeout (${idleTimeoutMs}): a drained box must SURVIVE its full warm window so ` +
|
|
5781
|
+
`the reaper can resume + snapshot /workspace + terminate it on the sweep AFTER the drain grace ` +
|
|
5782
|
+
`elapses — Modal's idle-reap must NOT fire first (or /workspace is lost). Raise ` +
|
|
5783
|
+
`OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS (defaults to OPENGENI_MODAL_TIMEOUT_SECONDS) or lower ` +
|
|
5784
|
+
`OPENGENI_SANDBOX_IDLE_GRACE_MS.`,
|
|
5785
|
+
);
|
|
5786
|
+
}
|
|
5217
5787
|
}
|
|
5218
5788
|
}
|
|
5219
5789
|
// --- stream-token secret: required-when-desktop, but GRACEFULLY DEGRADE (stream-token availability contract) ---
|
|
@@ -5224,6 +5794,20 @@ function validateSettings(settings: Settings): void {
|
|
|
5224
5794
|
// negotiateCapabilities degrades the desktop cell). This keeps a desktop-
|
|
5225
5795
|
// configured deployment bootable (headless + Channel-A still work) instead of
|
|
5226
5796
|
// crashing the whole API on a missing secret.
|
|
5797
|
+
if (
|
|
5798
|
+
settings.sandboxDesktopEnabled &&
|
|
5799
|
+
settings.sandboxBackend === "modal" &&
|
|
5800
|
+
!["local", "test"].includes(settings.environment)
|
|
5801
|
+
) {
|
|
5802
|
+
if (!isDigestPinnedModalDesktopImage(settings)) {
|
|
5803
|
+
throw new Error(
|
|
5804
|
+
"OPENGENI_MODAL_IMAGE_REF must be digest-pinned (registry/name@sha256:…) when " +
|
|
5805
|
+
"OPENGENI_SANDBOX_BACKEND=modal and OPENGENI_SANDBOX_DESKTOP_ENABLED=true. " +
|
|
5806
|
+
"Computer/Browser need docker/desktop.Dockerfile, not the official headless " +
|
|
5807
|
+
"opengeni-sandbox image. Helm desktop.imageRef writes this pin.",
|
|
5808
|
+
);
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5227
5811
|
if (settings.sandboxDesktopEnabled && resolveStreamTokenSecret(settings) === undefined) {
|
|
5228
5812
|
console.warn(
|
|
5229
5813
|
"[opengeni] OPENGENI_SANDBOX_DESKTOP_ENABLED=true but neither OPENGENI_STREAM_TOKEN_SECRET nor " +
|
|
@@ -5235,10 +5819,12 @@ function validateSettings(settings: Settings): void {
|
|
|
5235
5819
|
// Model provider registry: parse it here so JSON/zod errors surface at boot,
|
|
5236
5820
|
// reject a registry id colliding with the built-in provider id (it would
|
|
5237
5821
|
// shadow the built-in in configuredProviders), reject duplicate registry
|
|
5238
|
-
// ids, and
|
|
5239
|
-
//
|
|
5240
|
-
//
|
|
5241
|
-
//
|
|
5822
|
+
// ids, and preserve the existing key requirement for every registry provider
|
|
5823
|
+
// except connected Codex and the explicit anonymous opt-in. Anonymous
|
|
5824
|
+
// providers are externally metered; a missing key on every other ordinary
|
|
5825
|
+
// provider remains a boot error.
|
|
5826
|
+
// Registry models flow through configuredAllowedModels, so the managed-billing
|
|
5827
|
+
// pricing check above already covers the OpenGeni-credit providers.
|
|
5242
5828
|
const registryProviders = parseModelProvidersJson(settings.modelProvidersJson);
|
|
5243
5829
|
const builtinId = builtinProviderId(settings);
|
|
5244
5830
|
const providerIds = new Set<string>();
|
|
@@ -5263,7 +5849,11 @@ function validateSettings(settings: Settings): void {
|
|
|
5263
5849
|
);
|
|
5264
5850
|
}
|
|
5265
5851
|
providerIds.add(provider.id);
|
|
5266
|
-
if (
|
|
5852
|
+
if (
|
|
5853
|
+
provider.kind !== "codex-subscription" &&
|
|
5854
|
+
provider.kind !== "anonymous" &&
|
|
5855
|
+
!resolveProviderApiKey(provider)
|
|
5856
|
+
) {
|
|
5267
5857
|
throw new Error(
|
|
5268
5858
|
`OPENGENI_MODEL_PROVIDERS_JSON provider ${provider.id} requires a resolvable API key (set apiKey or apiKeyEnv)`,
|
|
5269
5859
|
);
|
|
@@ -5373,7 +5963,7 @@ export function resolveNatsCalloutConfig(settings: Settings): NatsCalloutConfig
|
|
|
5373
5963
|
* The PRIVILEGED control-plane NATS login (api/worker). Present only when BOTH a
|
|
5374
5964
|
* user and password are set; otherwise null and the bus connects anonymously (local
|
|
5375
5965
|
* dev / a NATS without auth_callout). When the callout plane is on, this is the
|
|
5376
|
-
* static account user permitted to request
|
|
5966
|
+
* static account user permitted to request exact generation-fenced agent RPC subjects.
|
|
5377
5967
|
*/
|
|
5378
5968
|
export interface NatsControlPlaneAuth {
|
|
5379
5969
|
user: string;
|