@opengeni/config 0.16.2 → 0.19.0-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/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.14"),
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,56 @@ 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 pathology detection (no-progress
382
- // streaks, budget exhaustion), never by count. goalMaxAutoContinuations is
383
- // therefore UNSET by default (no cap); deployments may configure one, and
384
- // it then acts as a hard ceiling that per-goal overrides can only lower.
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
- goalNoProgressLimit: z.coerce.number().int().positive().default(3),
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 off: with the flag
491
+ // off the routing resolver short-circuits to the installation's own workspace
492
+ // before any new read, so an existing single-workspace install behaves exactly
493
+ // as it did. Enabling it is a deploy decision, not a code default.
494
+ // Env: OPENGENI_SLACK_WORKSPACE_ROUTING_ENABLED.
495
+ slackWorkspaceRoutingEnabled: EnvBoolean.default(false),
387
496
  // Per-segment ceiling on agent loop turns (model calls) within a single
388
497
  // session turn. Effectively unbounded by default for the same reason as
389
498
  // above; the graceful max-turns valve (idle + goal continuation, never a
390
499
  // session failure) remains as inert safety should a deployment set a cap.
391
500
  agentMaxModelCallsPerTurn: z.coerce.number().int().positive().default(1_000_000),
392
- // The model family's real context window in tokens. OpenGeni always performs
393
- // one durable, portable plaintext compaction transition; there is no
394
- // provider/server/off mode ladder.
501
+ // Deployment fallback for models that do not declare their own window.
502
+ // Built-in billed GPT-5.6 Sol/Terra/Luna pin Codex's 272k catalog instead.
503
+ // OpenGeni always performs one durable, portable plaintext compaction
504
+ // transition; there is no provider/server/off mode ladder.
395
505
  contextWindowTokens: z.coerce.number().int().positive().default(1_050_000),
396
- // Optional model-catalog effective input ceiling. Codex models expose this as
397
- // raw context_window * effective_context_window_percent; when absent, retain
398
- // the deployment-level window-minus-reserved-output behavior.
506
+ // Optional model-catalog effective input ceiling. Codex and billed GPT-5.6
507
+ // models expose this as raw context_window * effective_context_window_percent;
508
+ // when absent, retain the deployment-level window-minus-reserved-output
509
+ // behavior.
399
510
  contextEffectiveWindowTokens: z.coerce.number().int().positive().optional(),
400
511
  // Proactive compaction threshold as a ratio of the model context window.
401
512
  // Defaults to 90%: compact as late as possible — retained context beats early
@@ -430,6 +541,10 @@ const SettingsSchema = z.object({
430
541
  apiHost: z.string().default("0.0.0.0"),
431
542
  apiPort: z.coerce.number().int().positive().default(8000),
432
543
  workerHttpPort: z.coerce.number().int().positive().default(8001),
544
+ // Worker-side first-party MCP traffic stays on the deployment's internal
545
+ // network. OPENGENI_MCP_URL remains the sandbox/external route used by
546
+ // Codemode and remote placements.
547
+ opengeniMcpInternalUrl: z.string().url().optional(),
433
548
  opengeniMcpUrl: z.string().url().optional(),
434
549
  // Origins allowed to send browser cookies cross-origin. Other origins may
435
550
  // call the public API with bearer credentials, but never receive credentialed
@@ -564,18 +679,27 @@ const SettingsSchema = z.object({
564
679
  // SuperGrok/xAI connected subscription. This is a workspace-scoped OAuth
565
680
  // account pool and a distinct rail from the existing xai/* API-key provider.
566
681
  supergrokSubscriptionEnabled: EnvBoolean.default(false), // OPENGENI_SUPERGROK_SUBSCRIPTION_ENABLED
682
+ // Maximum silence between complete, valid SuperGrok SSE data events. This is
683
+ // not a request/run duration cap; every valid event resets the timer.
684
+ supergrokResponseStreamIdleTimeoutMs: z.coerce
685
+ .number()
686
+ .int()
687
+ .positive()
688
+ .max(24 * 60 * 60_000)
689
+ .default(XAI_RESPONSE_STREAM_IDLE_TIMEOUT_MS),
567
690
  // Expose the connected apps attached to a Codex subscription through the
568
691
  // synthetic codex_apps MCP server. Independent from subscription routing so
569
692
  // operators can use Codex models without exposing ChatGPT connectors.
570
693
  codexConnectedAppsEnabled: EnvBoolean.default(false), // OPENGENI_CODEX_CONNECTED_APPS_ENABLED
571
694
  codexProductSku: z.string().optional(), // OPENGENI_CODEX_PRODUCT_SKU (X-OpenAI-Product-Sku, apps only)
572
695
  // Progressive MCP disclosure (Codex-CLI-style tool_search): on a codex turn,
573
- // flag non-mandatory selected MCP tools `defer_loading:true` (dropping their
696
+ // flag non-eager selected MCP tools `defer_loading:true` (dropping their
574
697
  // schemas from model context) and add one client-executed tool_search tool
575
- // that BM25-discloses bounded matches. The mandatory OpenGeni tools stay
576
- // eager. Default ON so selected connector catalogues do not consume every
577
- // Codex turn's context. Operators may explicitly disable it for emergency
578
- // compatibility diagnosis.
698
+ // that BM25-discloses bounded matches. Only an exact session tool ref with
699
+ // `eager:true` stays on the startup path; mandatory selection alone does not
700
+ // imply eagerness. Default ON so selected connector catalogues do not consume
701
+ // every Codex turn's context. Operators may explicitly disable it for
702
+ // emergency compatibility diagnosis.
579
703
  // OPENGENI_CODEX_TOOL_SEARCH_ENABLED
580
704
  codexToolSearchEnabled: EnvBoolean.default(true),
581
705
  // Provider-neutral progressive disclosure for direct OpenAI/Azure native
@@ -814,6 +938,39 @@ const SettingsSchema = z.object({
814
938
  vercelProjectId: z.string().optional(),
815
939
  vercelTeamId: z.string().optional(),
816
940
  vercelRuntime: z.string().optional(),
941
+ // --- OpenSandbox (optional Kubernetes-native provisioned sandbox) ---
942
+ openSandboxBaseUrl: z.string().url().optional(),
943
+ openSandboxApiKey: z.string().min(1).optional(),
944
+ // Release and preview profiles must provide an immutable OCI digest. The
945
+ // adapter refuses tag-only references when this backend is active.
946
+ openSandboxImage: z.string().min(1).optional(),
947
+ // Renewable provider TTL is a leak/backstop clock, not OpenGeni's idle
948
+ // policy. The pinned server accepts a one-minute minimum; ordinary
949
+ // deployments default to one hour.
950
+ openSandboxTtlSeconds: z.coerce.number().int().min(60).max(86_400).default(3_600),
951
+ openSandboxUseServerProxy: EnvBoolean.default(true),
952
+ // Channel B (browserd / noVNC / ttyd) uses OSEP-0011 signed URI-mode ingress.
953
+ // Exec/files stay on the private lifecycle server-proxy regardless of this flag.
954
+ openSandboxSignedEndpoints: EnvBoolean.default(false),
955
+ openSandboxSignedEndpointTtlSeconds: z.coerce.number().int().min(60).max(3_600).default(600),
956
+ openSandboxChannelBPublicBaseUrl: z.string().url().optional(),
957
+ // Emergency hatch only: force JPEG/RFB through the API frame-proxy even when
958
+ // signed endpoints are on (M2 subprotocol failure). Unset means OpenSandbox
959
+ // uses the frame-proxy unless signed endpoints are on.
960
+ openSandboxInteractionFrameProxy: EnvBoolean.optional(),
961
+ openSandboxPoolRef: z
962
+ .string()
963
+ .regex(/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/)
964
+ .optional(),
965
+ // Optional same-cluster, read-only observability projection. The application
966
+ // chart sets this only on the control worker and mounts a dedicated projected
967
+ // service-account token; non-Kubernetes and remote-provider deployments omit it.
968
+ openSandboxKubernetesInventoryNamespace: z
969
+ .string()
970
+ .min(1)
971
+ .max(63)
972
+ .regex(/^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?$/)
973
+ .optional(),
817
974
  // --- sandbox ownership inversion (P1.2 rollout flag, default OFF) ---
818
975
  // The keystone flag for the stateless resume-by-id model. When FALSE the
819
976
  // agent-turn path is BYTE-FOR-BYTE today's build-and-discard behavior (no
@@ -858,7 +1015,7 @@ const SettingsSchema = z.object({
858
1015
  // runner must ALSO advertise Capabilities.op_stream. Streaming is the default
859
1016
  // because it is the only transport that can keep a command alive without an
860
1017
  // arbitrary request/reply wall while still supporting replay and cancellation.
861
- // Older runners remain usable when an explicit positive exec timeout is set.
1018
+ // Exec fails closed when the deployment or runner does not provide op-stream.
862
1019
  // EnvBoolean (NOT
863
1020
  // z.coerce.boolean(), which coerces "false" -> true).
864
1021
  agentOpStreamEnabled: EnvBoolean.default(true),
@@ -890,9 +1047,10 @@ const SettingsSchema = z.object({
890
1047
  // nats-server is configured with AUTH CALLOUT: an external agent connects
891
1048
  // presenting its `oge_` enrollment bearer as the connect auth-token; the server
892
1049
  // issues an authorization request on $SYS.REQ.USER.AUTH to our responder, which
893
- // validates the bearer and returns a SIGNED NATS user JWT scoped to pub/sub ONLY
894
- // `agent.<ws>.>` (+ `_INBOX.>`). That per-subject scope IS the per-workspace
895
- // isolation. These are deployment-level secrets in the opengeni-runtime secret
1050
+ // validates the bearer, claims one daemon generation, and returns a SIGNED NATS
1051
+ // user JWT scoped to that exact process subtree (+ `_INBOX.>`). The exact scope
1052
+ // provides both workspace isolation and single-daemon routing authority. These
1053
+ // are deployment-level secrets in the opengeni-runtime secret
896
1054
  // (Helm-clobbered configmap avoided), all OPTIONAL: when the callout plane is not
897
1055
  // configured the responder simply does not start (selfhosted agents cannot
898
1056
  // connect — graceful, never a boot-fail).
@@ -904,7 +1062,7 @@ const SettingsSchema = z.object({
904
1062
  // The TARGET ACCOUNT NAME the minted user is placed into (the server-config-mode
905
1063
  // `auth_callout.account`, e.g. "APP"). The responder writes it as the minted user
906
1064
  // JWT `aud` so nats-server binds the agent to this account — the SAME account the
907
- // privileged control plane connects into, so `agent.<ws>.<id>.rpc` request/reply
1065
+ // privileged control plane connects into, so exact process request/reply
908
1066
  // routes. Optional; resolveNatsCalloutConfig defaults it to "APP".
909
1067
  selfhostedNatsCalloutAccountName: z.string().optional(),
910
1068
  // The callout RESPONDER's own NATS login (one of the `auth_callout.auth_users`
@@ -913,7 +1071,7 @@ const SettingsSchema = z.object({
913
1071
  selfhostedNatsCalloutUser: z.string().optional(),
914
1072
  selfhostedNatsCalloutPassword: z.string().optional(),
915
1073
  // The PRIVILEGED control-plane login (api/worker): a static account user that may
916
- // request `agent.*.rpc` + receive its inbox replies. The event bus + the
1074
+ // request exact process RPC subjects + receive their inbox replies. The event bus + the
917
1075
  // selfhosted control RPC ride THIS connection. Username/password; when unset the
918
1076
  // bus connects anonymously (local dev / a NATS with no auth_callout).
919
1077
  selfhostedNatsControlUser: z.string().optional(),
@@ -997,6 +1155,21 @@ const SettingsSchema = z.object({
997
1155
  // (a liveness/reaper cadence), this bounds how long one turn waits for capacity
998
1156
  // or provider creation before surfacing a clear turn.failed error.
999
1157
  sandboxWarmingTimeoutMs: z.coerce.number().int().positive().default(600_000),
1158
+ // Request-scoped workspace control-prefix budget: how long one HTTP-originated
1159
+ // session/workspace mutation (Send, Steer, Pause/Resume/Cancel, queue
1160
+ // move/edit/delete, composer draft, settings narrowing, quiescent tree
1161
+ // deletion) may wait to enter the fair `workspace_inference_controls` prefix
1162
+ // before failing with the retryable 503 `WORKSPACE_CONTROL_BUSY`. Worker
1163
+ // settlement and claims never use it. The API installs the validated value
1164
+ // into @opengeni/db once at app construction; nothing reads the env per
1165
+ // request. Env: OPENGENI_WORKSPACE_CONTROL_LOCK_TIMEOUT_MS. Default 20 s.
1166
+ workspaceControlLockTimeoutMs: z.coerce
1167
+ .number({
1168
+ message: "OPENGENI_WORKSPACE_CONTROL_LOCK_TIMEOUT_MS must be a positive integer (ms)",
1169
+ })
1170
+ .int("OPENGENI_WORKSPACE_CONTROL_LOCK_TIMEOUT_MS must be a positive integer (ms)")
1171
+ .positive("OPENGENI_WORKSPACE_CONTROL_LOCK_TIMEOUT_MS must be a positive integer (ms)")
1172
+ .default(20_000),
1000
1173
  // Rig setup-script budget (M3): the wall-clock timeout the rig-setup lifecycle
1001
1174
  // hook runs its script under, distinct from the 120s per-command lifecycle
1002
1175
  // default (a rig may compile/install heavy tooling on first cold create).
@@ -1056,6 +1229,11 @@ const SettingsSchema = z.object({
1056
1229
  githubAppId: z.string().optional(),
1057
1230
  githubClientId: z.string().optional(),
1058
1231
  githubClientSecret: z.string().optional(),
1232
+ /** Default-off rollout for the in-process GitHub repository API tool surface. */
1233
+ githubRestMcpEnabled: EnvBoolean.default(false),
1234
+ githubPersonalOauthEnabled: EnvBoolean.default(false),
1235
+ githubPersonalOauthClientId: z.string().optional(),
1236
+ githubPersonalOauthClientSecret: z.string().optional(),
1059
1237
  githubAppSlug: z.string().optional(),
1060
1238
  githubWebhookSecret: z.string().optional(),
1061
1239
  githubAppPrivateKey: z.string().optional(),
@@ -1096,6 +1274,76 @@ const SettingsSchema = z.object({
1096
1274
  export type Settings = z.infer<typeof SettingsSchema>;
1097
1275
  export type McpServerConfig = Settings["mcpServers"][number];
1098
1276
 
1277
+ export type GoogleDriveProviderRetryOptions = {
1278
+ requestTimeoutMs: number;
1279
+ attempts: number;
1280
+ initialDelayMs: number;
1281
+ maxDelayMs: number;
1282
+ budgetMs: number;
1283
+ };
1284
+
1285
+ /** Freeze one validated provider-neutral budget into every newly created or
1286
+ * updated Google Drive knowledge-source schedule. Existing schedules retain
1287
+ * their persisted limits until an authorized source save updates them. */
1288
+ export function configuredGoogleDriveSyncLimits(settings: Settings) {
1289
+ return KnowledgeSourceSyncLimits.parse({
1290
+ maxItems: settings.googleDriveSyncMaxItems,
1291
+ maxBytes: settings.googleDriveSyncMaxBytes,
1292
+ maxFileBytes: settings.googleDriveSyncMaxFileBytes,
1293
+ maxProviderRequests: settings.googleDriveSyncMaxProviderRequests,
1294
+ maxElapsedSeconds: settings.googleDriveSyncMaxElapsedSeconds,
1295
+ maxFailureDetails: settings.googleDriveSyncMaxFailureDetails,
1296
+ });
1297
+ }
1298
+
1299
+ /** Bounded in-activity retry policy for individual Google Drive requests. The
1300
+ * durable sync workflow remains authoritative after this local budget ends. */
1301
+ export function googleDriveProviderRetryOptions(
1302
+ settings: Settings,
1303
+ ): GoogleDriveProviderRetryOptions {
1304
+ return {
1305
+ requestTimeoutMs: settings.googleDriveProviderRequestTimeoutMs,
1306
+ attempts: settings.googleDriveProviderRetryAttempts,
1307
+ initialDelayMs: settings.googleDriveProviderRetryInitialDelayMs,
1308
+ maxDelayMs: settings.googleDriveProviderRetryMaxDelayMs,
1309
+ budgetMs: settings.googleDriveProviderRetryBudgetMs,
1310
+ };
1311
+ }
1312
+
1313
+ /** Return only a credential-free HTTP(S) origin. Reject path, user-info, query,
1314
+ * and fragment input instead of reflecting it into callbacks or evidence. */
1315
+ export function canonicalPublicOrigin(publicBaseUrl: string | undefined): string | null {
1316
+ if (!publicBaseUrl) return null;
1317
+ let parsed: URL;
1318
+ try {
1319
+ parsed = new URL(publicBaseUrl);
1320
+ } catch {
1321
+ return null;
1322
+ }
1323
+ if (
1324
+ !["http:", "https:"].includes(parsed.protocol) ||
1325
+ parsed.username ||
1326
+ parsed.password ||
1327
+ parsed.search ||
1328
+ parsed.hash ||
1329
+ (parsed.pathname !== "" && parsed.pathname !== "/")
1330
+ ) {
1331
+ return null;
1332
+ }
1333
+ return parsed.origin;
1334
+ }
1335
+
1336
+ export function googleDriveOAuthCallbackUrl(publicBaseUrl: string | undefined): string | null {
1337
+ const origin = canonicalPublicOrigin(publicBaseUrl);
1338
+ return origin ? `${origin}/v1/integrations/google-drive/callback` : null;
1339
+ }
1340
+
1341
+ /** Exact callback registered on the environment-specific personal GitHub OAuth App. */
1342
+ export function personalGitHubOAuthCallbackUrl(publicBaseUrl: string | undefined): string | null {
1343
+ const origin = canonicalPublicOrigin(publicBaseUrl);
1344
+ return origin ? `${origin}/v1/integrations/github-personal/oauth/callback` : null;
1345
+ }
1346
+
1099
1347
  /** Declarative voice-input transcription provider ids. */
1100
1348
  export type VoiceInputProviderId =
1101
1349
  | "openai"
@@ -1489,7 +1737,7 @@ export type ModelExecutionLimitsV1 = {
1489
1737
  };
1490
1738
 
1491
1739
  export type CredentialSourceV1 =
1492
- | { kind: "deployment"; mechanism: "api_key" | "azure_ad_bearer" }
1740
+ | { kind: "deployment"; mechanism: "api_key" | "azure_ad_bearer" | "none" }
1493
1741
  | { kind: "connected_subscription"; provider: "codex" | "xai" }
1494
1742
  | { kind: "workspace_connection"; mechanism: "api_key" };
1495
1743
 
@@ -1508,13 +1756,23 @@ export type BillingAttributionV1 = {
1508
1756
  export const ModelProviderApi = z.enum(["responses", "chat"]);
1509
1757
  export type ModelProviderApi = z.infer<typeof ModelProviderApi>;
1510
1758
 
1759
+ /**
1760
+ * Provider-specific request semantics that are independent of the endpoint's
1761
+ * OpenAI-compatible wire API. Secondary Azure resources still speak the
1762
+ * Responses API, but need Azure's stricter computer-call normalization.
1763
+ */
1764
+ export const ModelProviderWireProfile = z.enum(["openai", "azure-openai"]);
1765
+ export type ModelProviderWireProfile = z.infer<typeof ModelProviderWireProfile>;
1766
+
1511
1767
  /**
1512
1768
  * Registry provider kind. "api-key" providers carry their own static key/headers;
1513
- * connected-subscription providers resolve a workspace account token at call
1514
- * time and never carry a static key in the registry definition.
1769
+ * "anonymous" providers intentionally send no credential and are externally
1770
+ * metered; connected-subscription providers resolve a workspace account token
1771
+ * at call time and never carry a static key in the registry definition.
1515
1772
  */
1516
1773
  export const RegistryProviderKind = z.enum([
1517
1774
  "api-key",
1775
+ "anonymous",
1518
1776
  "codex-subscription",
1519
1777
  "xai-subscription",
1520
1778
  "vercel-gateway-managed",
@@ -1572,24 +1830,73 @@ const RegistryModelSchema = z
1572
1830
  });
1573
1831
 
1574
1832
  /** A non-built-in provider declared by the host via OPENGENI_MODEL_PROVIDERS_JSON. */
1575
- const RegistryProviderSchema = z.object({
1576
- kind: RegistryProviderKind.default("api-key"),
1577
- id: z.string().min(1).regex(registryId), // stable provider id, e.g. "fireworks"
1578
- label: z.string().min(1).optional(),
1579
- api: ModelProviderApi.default("chat"),
1580
- baseUrl: z.string().url(),
1581
- apiKey: z.string().optional(), // inline key (pragmatic) ...
1582
- apiKeyEnv: z.string().optional(), // ... OR name of the env var holding the key (preferred)
1583
- defaultQuery: z.record(z.string(), z.string()).optional(),
1584
- defaultHeaders: z.record(z.string(), z.string()).optional(),
1585
- publicDefaultQueryNames: z.array(z.string().min(1)).optional(),
1586
- publicDefaultHeaderNames: z.array(z.string().min(1)).optional(),
1587
- // V1 derives these from provider kind. Workspace BYOK is deliberately not a
1588
- // registry switch and requires a separately reviewed encrypted broker.
1589
- credentialSource: z.never().optional(),
1590
- billing: z.never().optional(),
1591
- models: z.array(RegistryModelSchema).min(1),
1592
- });
1833
+ const RegistryProviderSchema = z
1834
+ .object({
1835
+ kind: RegistryProviderKind.default("api-key"),
1836
+ id: z.string().min(1).regex(registryId), // stable provider id, e.g. "fireworks"
1837
+ label: z.string().min(1).optional(),
1838
+ api: ModelProviderApi.default("chat"),
1839
+ wireProfile: ModelProviderWireProfile.default("openai"),
1840
+ baseUrl: z.string().url(),
1841
+ apiKey: z.string().optional(), // inline key (pragmatic) ...
1842
+ apiKeyEnv: z.string().optional(), // ... OR name of the env var holding the key (preferred)
1843
+ defaultQuery: z.record(z.string(), z.string()).optional(),
1844
+ defaultHeaders: z.record(z.string(), z.string()).optional(),
1845
+ publicDefaultQueryNames: z.array(z.string().min(1)).optional(),
1846
+ publicDefaultHeaderNames: z.array(z.string().min(1)).optional(),
1847
+ // V1 derives these from provider kind. Workspace BYOK is deliberately not a
1848
+ // registry switch and requires a separately reviewed encrypted broker.
1849
+ credentialSource: z.never().optional(),
1850
+ billing: z.never().optional(),
1851
+ models: z.array(RegistryModelSchema).min(1),
1852
+ })
1853
+ .superRefine((provider, ctx) => {
1854
+ if (provider.kind !== "anonymous") {
1855
+ return;
1856
+ }
1857
+ if (provider.apiKey !== undefined) {
1858
+ ctx.addIssue({
1859
+ code: "custom",
1860
+ path: ["apiKey"],
1861
+ message: "anonymous providers must not declare apiKey",
1862
+ });
1863
+ }
1864
+ if (provider.apiKeyEnv !== undefined) {
1865
+ ctx.addIssue({
1866
+ code: "custom",
1867
+ path: ["apiKeyEnv"],
1868
+ message: "anonymous providers must not declare apiKeyEnv",
1869
+ });
1870
+ }
1871
+ if (provider.defaultHeaders !== undefined) {
1872
+ ctx.addIssue({
1873
+ code: "custom",
1874
+ path: ["defaultHeaders"],
1875
+ message: "anonymous providers must not declare defaultHeaders",
1876
+ });
1877
+ }
1878
+ if (provider.defaultQuery !== undefined) {
1879
+ ctx.addIssue({
1880
+ code: "custom",
1881
+ path: ["defaultQuery"],
1882
+ message: "anonymous providers must not declare defaultQuery",
1883
+ });
1884
+ }
1885
+ if (provider.publicDefaultHeaderNames !== undefined) {
1886
+ ctx.addIssue({
1887
+ code: "custom",
1888
+ path: ["publicDefaultHeaderNames"],
1889
+ message: "anonymous providers must not declare publicDefaultHeaderNames",
1890
+ });
1891
+ }
1892
+ if (provider.publicDefaultQueryNames !== undefined) {
1893
+ ctx.addIssue({
1894
+ code: "custom",
1895
+ path: ["publicDefaultQueryNames"],
1896
+ message: "anonymous providers must not declare publicDefaultQueryNames",
1897
+ });
1898
+ }
1899
+ });
1593
1900
  export type RegistryProvider = z.infer<typeof RegistryProviderSchema>;
1594
1901
 
1595
1902
  export const IntegrationOAuthClientConfigSchema = z.object({
@@ -1611,8 +1918,9 @@ export type IntegrationOAuthClientConfig = z.infer<typeof IntegrationOAuthClient
1611
1918
  export interface ResolvedModelProvider {
1612
1919
  id: string; // "openai" | "azure" | registry id
1613
1920
  label: string;
1614
- kind: RegistryProviderKind; // "api-key" (built-ins + most registry) | "codex-subscription"
1921
+ kind: RegistryProviderKind; // "api-key" (built-ins + most registry) | "anonymous" | subscription
1615
1922
  api: ModelProviderApi;
1923
+ wireProfile: ModelProviderWireProfile;
1616
1924
  builtin: boolean;
1617
1925
  baseUrl?: string | undefined;
1618
1926
  apiKey?: string | undefined;
@@ -1883,6 +2191,11 @@ export const SANDBOX_REQUIRED_ENV: Record<
1883
2191
  { field: "vercelToken", env: "OPENGENI_VERCEL_TOKEN" },
1884
2192
  { field: "vercelProjectId", env: "OPENGENI_VERCEL_PROJECT_ID" },
1885
2193
  ],
2194
+ opensandbox: [
2195
+ { field: "openSandboxBaseUrl", env: "OPENGENI_OPENSANDBOX_BASE_URL" },
2196
+ { field: "openSandboxApiKey", env: "OPENGENI_OPENSANDBOX_API_KEY" },
2197
+ { field: "openSandboxImage", env: "OPENGENI_OPENSANDBOX_IMAGE" },
2198
+ ],
1886
2199
  // selfhosted needs NO per-box credentials: it is the user's own machine reached
1887
2200
  // over the agent's own enrollment. The enrollment-signing + relay-token secrets
1888
2201
  // are deployment-level (a single runtime secret, not per-active-backend creds),
@@ -1895,6 +2208,34 @@ export function requiredSandboxEnvForBackend(backend: z.infer<typeof SandboxBack
1895
2208
  return (SANDBOX_REQUIRED_ENV[backend] ?? []).map((entry) => entry.env);
1896
2209
  }
1897
2210
 
2211
+ function objectStorageConfiguredForWorkspaceArchives(settings: Settings): boolean {
2212
+ switch (settings.objectStorageBackend) {
2213
+ case "azure-blob":
2214
+ return Boolean(
2215
+ settings.objectStorageAzureConnectionString ||
2216
+ (settings.objectStorageAzureAccountName && settings.objectStorageAzureAccountKey),
2217
+ );
2218
+ case "gcs":
2219
+ return Boolean(
2220
+ settings.objectStorageGcsCredentialsJson ||
2221
+ settings.objectStorageGcsKeyFilename ||
2222
+ settings.objectStorageGcsProjectId,
2223
+ );
2224
+ case "aws-s3":
2225
+ return true;
2226
+ case "s3-compatible":
2227
+ return Boolean(
2228
+ settings.objectStorageEndpoint &&
2229
+ settings.objectStorageAccessKeyId &&
2230
+ settings.objectStorageSecretAccessKey,
2231
+ );
2232
+ default: {
2233
+ const _exhaustive: never = settings.objectStorageBackend;
2234
+ return _exhaustive;
2235
+ }
2236
+ }
2237
+ }
2238
+
1898
2239
  function optional(name: string): string | undefined {
1899
2240
  const value = process.env[name];
1900
2241
  return value && value.trim().length > 0 ? value : undefined;
@@ -1955,6 +2296,9 @@ export function getSettings(): Settings {
1955
2296
  agentStableVersion: optional("OPENGENI_AGENT_STABLE_VERSION"),
1956
2297
  agentBetaVersion: optional("OPENGENI_AGENT_BETA_VERSION"),
1957
2298
  productAccessMode: optional("OPENGENI_PRODUCT_ACCESS_MODE"),
2299
+ organizationTenancyCanonicalActivationEnabled: optional(
2300
+ "OPENGENI_ORGANIZATION_TENANCY_CANONICAL_ACTIVATION_ENABLED",
2301
+ ),
1958
2302
  billingMode: optional("OPENGENI_BILLING_MODE"),
1959
2303
  entitlementsMode: optional("OPENGENI_ENTITLEMENTS_MODE"),
1960
2304
  usageLimitsMode: optional("OPENGENI_USAGE_LIMITS_MODE"),
@@ -1965,7 +2309,6 @@ export function getSettings(): Settings {
1965
2309
  allowedFirstPartyMcpTools: optional("OPENGENI_ALLOWED_FIRST_PARTY_MCP_TOOLS"),
1966
2310
  streamTokenSecret: optional("OPENGENI_STREAM_TOKEN_SECRET"),
1967
2311
  streamControlEnabled: optional("OPENGENI_STREAM_CONTROL_ENABLED"),
1968
- codemodeMaxCallsPerTurn: optional("OPENGENI_CODEMODE_MAX_CALLS_PER_TURN"),
1969
2312
  ogtoolPackageSpec: optional("OPENGENI_OGTOOL_PACKAGE_SPEC"),
1970
2313
  environmentsEncryptionKey: optional("OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY"),
1971
2314
  integrationsEnabled: optional("OPENGENI_INTEGRATIONS_ENABLED"),
@@ -1974,12 +2317,32 @@ export function getSettings(): Settings {
1974
2317
  "OPENGENI_INTEGRATIONS_ALLOW_PRIVATE_NETWORK_TARGETS",
1975
2318
  ),
1976
2319
  integrationsOauthClientsJson: optional("OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON"),
1977
- gmailRestAdapterEnabled: optional("OPENGENI_GMAIL_REST_ADAPTER_ENABLED"),
1978
2320
  slackClientId: optional("OPENGENI_SLACK_CLIENT_ID"),
1979
2321
  slackClientSecret: optional("OPENGENI_SLACK_CLIENT_SECRET"),
1980
2322
  slackSigningSecret: optional("OPENGENI_SLACK_SIGNING_SECRET"),
2323
+ slackBotDisplayName: optional("OPENGENI_SLACK_BOT_DISPLAY_NAME"),
2324
+ slackCommand: optional("OPENGENI_SLACK_COMMAND"),
1981
2325
  googleDriveClientId: optional("OPENGENI_GOOGLE_DRIVE_CLIENT_ID"),
1982
2326
  googleDriveClientSecret: optional("OPENGENI_GOOGLE_DRIVE_CLIENT_SECRET"),
2327
+ googleDriveSyncMaxItems: optional("OPENGENI_GOOGLE_DRIVE_SYNC_MAX_ITEMS"),
2328
+ googleDriveSyncMaxBytes: optional("OPENGENI_GOOGLE_DRIVE_SYNC_MAX_BYTES"),
2329
+ googleDriveSyncMaxFileBytes: optional("OPENGENI_GOOGLE_DRIVE_SYNC_MAX_FILE_BYTES"),
2330
+ googleDriveSyncMaxProviderRequests: optional(
2331
+ "OPENGENI_GOOGLE_DRIVE_SYNC_MAX_PROVIDER_REQUESTS",
2332
+ ),
2333
+ googleDriveSyncMaxElapsedSeconds: optional("OPENGENI_GOOGLE_DRIVE_SYNC_MAX_ELAPSED_SECONDS"),
2334
+ googleDriveSyncMaxFailureDetails: optional("OPENGENI_GOOGLE_DRIVE_SYNC_MAX_FAILURE_DETAILS"),
2335
+ googleDriveProviderRequestTimeoutMs: optional(
2336
+ "OPENGENI_GOOGLE_DRIVE_PROVIDER_REQUEST_TIMEOUT_MS",
2337
+ ),
2338
+ googleDriveProviderRetryAttempts: optional("OPENGENI_GOOGLE_DRIVE_PROVIDER_RETRY_ATTEMPTS"),
2339
+ googleDriveProviderRetryInitialDelayMs: optional(
2340
+ "OPENGENI_GOOGLE_DRIVE_PROVIDER_RETRY_INITIAL_DELAY_MS",
2341
+ ),
2342
+ googleDriveProviderRetryMaxDelayMs: optional(
2343
+ "OPENGENI_GOOGLE_DRIVE_PROVIDER_RETRY_MAX_DELAY_MS",
2344
+ ),
2345
+ googleDriveProviderRetryBudgetMs: optional("OPENGENI_GOOGLE_DRIVE_PROVIDER_RETRY_BUDGET_MS"),
1983
2346
  fikenClientId: optional("OPENGENI_FIKEN_OAUTH_CLIENT_ID"),
1984
2347
  fikenClientSecret: optional("OPENGENI_FIKEN_OAUTH_CLIENT_SECRET"),
1985
2348
  googleDriveWorkspaceEventsEnabled: optional("OPENGENI_GOOGLE_DRIVE_WORKSPACE_EVENTS_ENABLED"),
@@ -1988,7 +2351,10 @@ export function getSettings(): Settings {
1988
2351
  maxNestedAgentDepth: optional("OPENGENI_MAX_NESTED_AGENT_DEPTH"),
1989
2352
  socialOauthClientsJson: optional("OPENGENI_SOCIAL_OAUTH_CLIENTS_JSON"),
1990
2353
  goalMaxAutoContinuations: optional("OPENGENI_GOAL_MAX_AUTO_CONTINUATIONS"),
1991
- goalNoProgressLimit: optional("OPENGENI_GOAL_NO_PROGRESS_LIMIT"),
2354
+ goalIdleBackoffMs: optional("OPENGENI_GOAL_IDLE_BACKOFF_MS"),
2355
+ goalIdleBackoffMaxMs: optional("OPENGENI_GOAL_IDLE_BACKOFF_MAX_MS"),
2356
+ childLifecycleNoticesEnabled: optional("OPENGENI_CHILD_LIFECYCLE_NOTICES_ENABLED"),
2357
+ slackWorkspaceRoutingEnabled: optional("OPENGENI_SLACK_WORKSPACE_ROUTING_ENABLED"),
1992
2358
  agentMaxModelCallsPerTurn: optional("OPENGENI_AGENT_MAX_MODEL_CALLS_PER_TURN"),
1993
2359
  contextWindowTokens: optional("OPENGENI_CONTEXT_WINDOW_TOKENS"),
1994
2360
  contextEffectiveWindowTokens: optional("OPENGENI_CONTEXT_EFFECTIVE_WINDOW_TOKENS"),
@@ -2003,6 +2369,7 @@ export function getSettings(): Settings {
2003
2369
  apiHost: optional("OPENGENI_API_HOST"),
2004
2370
  apiPort: optional("OPENGENI_API_PORT"),
2005
2371
  workerHttpPort: optional("OPENGENI_WORKER_HTTP_PORT"),
2372
+ opengeniMcpInternalUrl: optional("OPENGENI_MCP_INTERNAL_URL"),
2006
2373
  opengeniMcpUrl: optional("OPENGENI_MCP_URL"),
2007
2374
  corsAllowOriginRegex: optional("OPENGENI_CORS_ALLOW_ORIGIN_REGEX"),
2008
2375
  openaiProvider: optional("OPENGENI_OPENAI_PROVIDER"),
@@ -2059,6 +2426,9 @@ export function getSettings(): Settings {
2059
2426
  modelProvidersJson: optional("OPENGENI_MODEL_PROVIDERS_JSON"),
2060
2427
  codexSubscriptionEnabled: optional("OPENGENI_CODEX_SUBSCRIPTION_ENABLED"),
2061
2428
  supergrokSubscriptionEnabled: optional("OPENGENI_SUPERGROK_SUBSCRIPTION_ENABLED"),
2429
+ supergrokResponseStreamIdleTimeoutMs: optional(
2430
+ "OPENGENI_SUPERGROK_RESPONSE_STREAM_IDLE_TIMEOUT_MS",
2431
+ ),
2062
2432
  codexConnectedAppsEnabled: optional("OPENGENI_CODEX_CONNECTED_APPS_ENABLED"),
2063
2433
  codexToolSearchEnabled: optional("OPENGENI_CODEX_TOOL_SEARCH_ENABLED"),
2064
2434
  lazyToolSearchEnabled: optional("OPENGENI_LAZY_TOOL_SEARCH_ENABLED"),
@@ -2148,6 +2518,21 @@ export function getSettings(): Settings {
2148
2518
  vercelProjectId: optional("OPENGENI_VERCEL_PROJECT_ID"),
2149
2519
  vercelTeamId: optional("OPENGENI_VERCEL_TEAM_ID"),
2150
2520
  vercelRuntime: optional("OPENGENI_VERCEL_RUNTIME"),
2521
+ openSandboxBaseUrl: optional("OPENGENI_OPENSANDBOX_BASE_URL"),
2522
+ openSandboxApiKey: optional("OPENGENI_OPENSANDBOX_API_KEY"),
2523
+ openSandboxImage: optional("OPENGENI_OPENSANDBOX_IMAGE"),
2524
+ openSandboxTtlSeconds: optional("OPENGENI_OPENSANDBOX_TTL_SECONDS"),
2525
+ openSandboxUseServerProxy: optional("OPENGENI_OPENSANDBOX_USE_SERVER_PROXY"),
2526
+ openSandboxSignedEndpoints: optional("OPENGENI_OPENSANDBOX_SIGNED_ENDPOINTS"),
2527
+ openSandboxSignedEndpointTtlSeconds: optional(
2528
+ "OPENGENI_OPENSANDBOX_SIGNED_ENDPOINT_TTL_SECONDS",
2529
+ ),
2530
+ openSandboxChannelBPublicBaseUrl: optional("OPENGENI_OPENSANDBOX_CHANNEL_B_PUBLIC_BASE_URL"),
2531
+ openSandboxInteractionFrameProxy: optional("OPENGENI_OPENSANDBOX_INTERACTION_FRAME_PROXY"),
2532
+ openSandboxPoolRef: optional("OPENGENI_OPENSANDBOX_POOL_REF"),
2533
+ openSandboxKubernetesInventoryNamespace: optional(
2534
+ "OPENGENI_OPENSANDBOX_KUBERNETES_INVENTORY_NAMESPACE",
2535
+ ),
2151
2536
  sandboxOwnershipEnabled: optional("OPENGENI_SANDBOX_OWNERSHIP_ENABLED"),
2152
2537
  rigVerificationLeaseOwnershipEnabled: optional(
2153
2538
  "OPENGENI_RIG_VERIFICATION_LEASE_OWNERSHIP_ENABLED",
@@ -2179,6 +2564,7 @@ export function getSettings(): Settings {
2179
2564
  sandboxLeaseTtlMs: optional("OPENGENI_SANDBOX_LEASE_TTL_MS"),
2180
2565
  sandboxLeaseWarmingTtlMs: optional("OPENGENI_SANDBOX_LEASE_WARMING_TTL_MS"),
2181
2566
  sandboxWarmingTimeoutMs: optional("OPENGENI_SANDBOX_WARMING_TIMEOUT_MS"),
2567
+ workspaceControlLockTimeoutMs: optional("OPENGENI_WORKSPACE_CONTROL_LOCK_TIMEOUT_MS"),
2182
2568
  rigSetupTimeoutMs: optional("OPENGENI_RIG_SETUP_TIMEOUT_MS"),
2183
2569
  sandboxWarmRateMicrosPerSecondJson: optional(
2184
2570
  "OPENGENI_SANDBOX_WARM_RATE_MICROS_PER_SECOND_JSON",
@@ -2225,6 +2611,10 @@ export function getSettings(): Settings {
2225
2611
  githubAppId: optional("OPENGENI_GITHUB_APP_ID"),
2226
2612
  githubClientId: optional("OPENGENI_GITHUB_CLIENT_ID"),
2227
2613
  githubClientSecret: optional("OPENGENI_GITHUB_CLIENT_SECRET"),
2614
+ githubRestMcpEnabled: optional("OPENGENI_GITHUB_REST_MCP_ENABLED"),
2615
+ githubPersonalOauthEnabled: optional("OPENGENI_GITHUB_PERSONAL_OAUTH_ENABLED"),
2616
+ githubPersonalOauthClientId: optional("OPENGENI_GITHUB_PERSONAL_OAUTH_CLIENT_ID"),
2617
+ githubPersonalOauthClientSecret: optional("OPENGENI_GITHUB_PERSONAL_OAUTH_CLIENT_SECRET"),
2228
2618
  githubAppSlug: optional("OPENGENI_GITHUB_APP_SLUG"),
2229
2619
  githubWebhookSecret: optional("OPENGENI_GITHUB_WEBHOOK_SECRET"),
2230
2620
  githubAppPrivateKey: optional("OPENGENI_GITHUB_APP_PRIVATE_KEY"),
@@ -2244,11 +2634,11 @@ export function getSettings(): Settings {
2244
2634
  const settings = {
2245
2635
  ...parsed,
2246
2636
  sandboxIdleGraceMs:
2247
- raw.sandboxIdleGraceMs === undefined
2637
+ raw.sandboxIdleGraceMs === undefined && parsed.sandboxBackend === "modal"
2248
2638
  ? Math.min(900_000, Math.floor((parsed.modalTimeoutSeconds * 1000) / 2))
2249
2639
  : parsed.sandboxIdleGraceMs,
2250
2640
  sandboxRotationLeadMs:
2251
- raw.sandboxRotationLeadMs === undefined
2641
+ raw.sandboxRotationLeadMs === undefined && parsed.sandboxBackend === "modal"
2252
2642
  ? Math.min(3_600_000, Math.floor((parsed.modalTimeoutSeconds * 1000) / 2))
2253
2643
  : parsed.sandboxRotationLeadMs,
2254
2644
  mcpServers: ensureBuiltInMcpServers(parsed),
@@ -2321,6 +2711,45 @@ export function effectiveModalIdleTimeoutSeconds(settings: Settings): number {
2321
2711
  return settings.modalIdleTimeoutSeconds ?? settings.modalTimeoutSeconds;
2322
2712
  }
2323
2713
 
2714
+ export type EffectiveSandboxLifecycle = {
2715
+ hardLifetimeMs: number | null;
2716
+ renewableTtlSeconds: number | null;
2717
+ providerIdleTimeoutMs: number | null;
2718
+ rotationLeadMs: number | null;
2719
+ };
2720
+
2721
+ /** Resolve provider lifecycle clocks without teaching generic callers Modal or
2722
+ * OpenSandbox field names. Modal's returned values are exactly the pre-existing
2723
+ * hard/idle/rotation values; OpenSandbox instead exposes a renewable TTL and no
2724
+ * finite-deadline rotation. */
2725
+ export function effectiveSandboxLifecycle(
2726
+ settings: Settings,
2727
+ backend: z.infer<typeof SandboxBackend> = settings.sandboxBackend,
2728
+ ): EffectiveSandboxLifecycle {
2729
+ if (backend === "modal") {
2730
+ return {
2731
+ hardLifetimeMs: settings.modalTimeoutSeconds * 1000,
2732
+ renewableTtlSeconds: null,
2733
+ providerIdleTimeoutMs: effectiveModalIdleTimeoutSeconds(settings) * 1000,
2734
+ rotationLeadMs: settings.sandboxRotationLeadMs,
2735
+ };
2736
+ }
2737
+ if (backend === "opensandbox") {
2738
+ return {
2739
+ hardLifetimeMs: null,
2740
+ renewableTtlSeconds: settings.openSandboxTtlSeconds,
2741
+ providerIdleTimeoutMs: null,
2742
+ rotationLeadMs: null,
2743
+ };
2744
+ }
2745
+ return {
2746
+ hardLifetimeMs: CAPABILITY_DESCRIPTORS[backend].lifetime.hardLifetimeMs ?? null,
2747
+ renewableTtlSeconds: null,
2748
+ providerIdleTimeoutMs: null,
2749
+ rotationLeadMs: null,
2750
+ };
2751
+ }
2752
+
2324
2753
  /**
2325
2754
  * One shared upper bound for the durable provider-capture claim and for command
2326
2755
  * admission waiting behind it. The SDK request itself is bounded by
@@ -2733,6 +3162,7 @@ function gatewayRegistryProvider(
2733
3162
  // Model-specific compatibility stays at the reviewed request fence rather
2734
3163
  // than downgrading the whole provider wire.
2735
3164
  api: "responses",
3165
+ wireProfile: "openai",
2736
3166
  baseUrl: VERCEL_AI_GATEWAY_BASE_URL,
2737
3167
  ...(input.apiKey ? { apiKey: input.apiKey } : {}),
2738
3168
  models,
@@ -2841,18 +3271,37 @@ export function productShortLabelForModelId(modelId: string): string | null {
2841
3271
  }
2842
3272
  }
2843
3273
 
3274
+ const BUILTIN_GPT56_MODEL_IDS = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] as const;
3275
+
3276
+ function isBuiltinGpt56ModelId(modelId: string): boolean {
3277
+ return (BUILTIN_GPT56_MODEL_IDS as readonly string[]).includes(modelId);
3278
+ }
3279
+
3280
+ /** Billed GPT-5.6 uses the same raw/effective/auto-compact catalog as Codex. */
3281
+ function builtinContextLimitsForModel(
3282
+ settings: Settings,
3283
+ modelId: string,
3284
+ ): Pick<
3285
+ ConfiguredModel,
3286
+ "contextWindowTokens" | "effectiveContextWindowTokens" | "autoCompactTokenLimit"
3287
+ > {
3288
+ if (isBuiltinGpt56ModelId(modelId)) {
3289
+ return {
3290
+ contextWindowTokens: CODEX_MODEL_CONTEXT_WINDOW_TOKENS,
3291
+ effectiveContextWindowTokens: CODEX_MODEL_EFFECTIVE_CONTEXT_WINDOW_TOKENS,
3292
+ autoCompactTokenLimit: CODEX_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
3293
+ };
3294
+ }
3295
+ return { contextWindowTokens: settings.contextWindowTokens };
3296
+ }
3297
+
2844
3298
  function builtinLatencyModesForModel(modelId: string): Array<{
2845
3299
  id: z.infer<typeof ModelLatencyModeV1>;
2846
3300
  upstream: "supported" | "unsupported" | "unknown";
2847
3301
  runnable: boolean;
2848
3302
  billingMultiplierBps?: number;
2849
3303
  }> {
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
- ) {
3304
+ if (isBuiltinGpt56ModelId(modelId) || modelId.startsWith("codex/gpt-5.6-")) {
2856
3305
  return [
2857
3306
  { id: "standard", upstream: "supported", runnable: true },
2858
3307
  {
@@ -2882,7 +3331,7 @@ function builtinHostedImageGenerationForModel(settings: Settings, modelId: strin
2882
3331
  return (
2883
3332
  settings.openaiProvider === "openai" &&
2884
3333
  isDirectOpenAiApiBaseUrl(settings.openaiBaseUrl) &&
2885
- ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"].includes(modelId)
3334
+ isBuiltinGpt56ModelId(modelId)
2886
3335
  );
2887
3336
  }
2888
3337
 
@@ -2966,6 +3415,9 @@ function assertLatencyModeRunnable(
2966
3415
  }
2967
3416
 
2968
3417
  function registryCredentialSource(provider: RegistryProvider): CredentialSourceV1 {
3418
+ if (provider.kind === "anonymous") {
3419
+ return { kind: "deployment", mechanism: "none" };
3420
+ }
2969
3421
  if (provider.kind === "codex-subscription") {
2970
3422
  return { kind: "connected_subscription", provider: "codex" };
2971
3423
  }
@@ -2979,6 +3431,9 @@ function registryCredentialSource(provider: RegistryProvider): CredentialSourceV
2979
3431
  }
2980
3432
 
2981
3433
  function registryBilling(provider: RegistryProvider): BillingAttributionV1 {
3434
+ if (provider.kind === "anonymous") {
3435
+ return { upstreamPayer: "deployment", metering: "external" };
3436
+ }
2982
3437
  if (provider.kind === "codex-subscription" || provider.kind === "xai-subscription") {
2983
3438
  return { upstreamPayer: "connected_subscription", metering: "external" };
2984
3439
  }
@@ -3060,6 +3515,7 @@ function definitionVersionFor(
3060
3515
  provider: {
3061
3516
  adapterKind: provider.kind,
3062
3517
  wireApi: provider.api,
3518
+ wireProfile: provider.wireProfile,
3063
3519
  baseUrl: provider.baseUrl ?? null,
3064
3520
  defaultHeaders: requestMetadata.headers,
3065
3521
  defaultQuery: requestMetadata.query,
@@ -3107,6 +3563,7 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
3107
3563
  label: builtinProviderLabel(settings),
3108
3564
  kind: "api-key",
3109
3565
  api: "responses",
3566
+ wireProfile: settings.openaiProvider === "azure" ? "azure-openai" : "openai",
3110
3567
  builtin: true,
3111
3568
  credentialSource,
3112
3569
  billing: { upstreamPayer: "deployment", metering: "opengeni_credits" },
@@ -3127,6 +3584,7 @@ export function configuredProviders(settings: Settings): ResolvedModelProvider[]
3127
3584
  label: provider.label ?? provider.id,
3128
3585
  kind: provider.kind,
3129
3586
  api: provider.api,
3587
+ wireProfile: provider.wireProfile,
3130
3588
  builtin: false,
3131
3589
  baseUrl: provider.baseUrl,
3132
3590
  apiKey: resolveProviderApiKey(provider),
@@ -3157,6 +3615,7 @@ export function withCodexCatalogProvider(settings: Settings): Settings {
3157
3615
  id: CODEX_PROVIDER_ID,
3158
3616
  label: "Codex (ChatGPT subscription)",
3159
3617
  api: "responses",
3618
+ wireProfile: "openai",
3160
3619
  baseUrl: CODEX_PROVIDER_BASE_URL,
3161
3620
  models: CODEX_FALLBACK_MODEL_SLUGS.map((slug) => {
3162
3621
  const capabilities = {
@@ -3214,11 +3673,13 @@ export function withXaiSubscriptionCatalogProvider(settings: Settings): Settings
3214
3673
  id: XAI_SUBSCRIPTION_PROVIDER_ID,
3215
3674
  label: "SuperGrok (xAI subscription)",
3216
3675
  api: "responses",
3676
+ wireProfile: "openai",
3217
3677
  baseUrl: XAI_SUBSCRIPTION_PROXY_BASE_URL,
3218
3678
  models: XAI_SUBSCRIPTION_MODEL_SLUGS.map((slug) => {
3219
3679
  const capabilities = legacyModelCapabilities(settings, {
3220
3680
  reasoningEffort: true,
3221
3681
  hostedWebSearch: true,
3682
+ vision: true,
3222
3683
  });
3223
3684
  capabilities.reasoning.efforts = ["low", "medium", "high", "xhigh"];
3224
3685
  capabilities.reasoning.defaultEffort = "high";
@@ -3430,7 +3891,7 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
3430
3891
  billing: builtinProvider.billing,
3431
3892
  capabilities,
3432
3893
  ...(pricingSchedules[id] === undefined ? {} : { pricing: pricingSchedules[id] }),
3433
- contextWindowTokens: settings.contextWindowTokens,
3894
+ ...builtinContextLimitsForModel(settings, id),
3434
3895
  toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
3435
3896
  reasoningEffort: capabilities.reasoning.runnable,
3436
3897
  hostedWebSearch: capabilities.hostedTools.webSearch.runnable,
@@ -4678,18 +5139,11 @@ function ensureBuiltInMcpServers(settings: Settings): Settings["mcpServers"] {
4678
5139
  }
4679
5140
 
4680
5141
  /**
4681
- * The base URL of OpenGeni's own first-party MCP endpoint, as a `{workspaceId}`
4682
- * template the SINGLE source of truth for the `opengeniMcpUrl`-or-loopback
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.
5142
+ * The sandbox/external base URL of OpenGeni's first-party MCP endpoint, as a
5143
+ * `{workspaceId}` template. Codemode and remote placements use this route.
4687
5144
  *
4688
5145
  * 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.
5146
+ * - STANDALONE (unset): falls back to the loopback default.
4693
5147
  * - EMBEDDED / MOUNTED (must set): when OpenGeni's API is mounted as a host
4694
5148
  * sub-app under a prefix (e.g. `https://host/og/v1/...`), the loopback
4695
5149
  * default is WRONG — the worker runs in the host process and `127.0.0.1:
@@ -4705,8 +5159,19 @@ export function firstPartyMcpBaseUrl(settings: Settings): string {
4705
5159
  );
4706
5160
  }
4707
5161
 
4708
- export function firstPartyMcpWorkspaceUrl(settings: Settings, workspaceId: string): string {
4709
- const raw = firstPartyMcpBaseUrl(settings);
5162
+ /**
5163
+ * Worker-side first-party MCP base. It never inherits the public/tunnel URL:
5164
+ * an operator may intentionally expose that route to Modal or a Connected
5165
+ * Machine, while the worker should still use loopback or cluster service DNS.
5166
+ */
5167
+ export function firstPartyMcpInternalBaseUrl(settings: Settings): string {
5168
+ return (
5169
+ settings.opengeniMcpInternalUrl ??
5170
+ `http://127.0.0.1:${settings.apiPort}/v1/workspaces/{workspaceId}/mcp`
5171
+ );
5172
+ }
5173
+
5174
+ function scopedFirstPartyMcpUrl(raw: string, workspaceId: string): string {
4710
5175
  if (raw.includes("{workspaceId}")) {
4711
5176
  return raw.replaceAll("{workspaceId}", workspaceId);
4712
5177
  }
@@ -4717,6 +5182,14 @@ export function firstPartyMcpWorkspaceUrl(settings: Settings, workspaceId: strin
4717
5182
  return url.toString();
4718
5183
  }
4719
5184
 
5185
+ export function firstPartyMcpWorkspaceUrl(settings: Settings, workspaceId: string): string {
5186
+ return scopedFirstPartyMcpUrl(firstPartyMcpBaseUrl(settings), workspaceId);
5187
+ }
5188
+
5189
+ export function firstPartyMcpInternalWorkspaceUrl(settings: Settings, workspaceId: string): string {
5190
+ return scopedFirstPartyMcpUrl(firstPartyMcpInternalBaseUrl(settings), workspaceId);
5191
+ }
5192
+
4720
5193
  export function codemodeWorkspaceUrl(settings: Settings, workspaceId: string): string {
4721
5194
  if (settings.opengeniMcpUrl) {
4722
5195
  const url = new URL(firstPartyMcpWorkspaceUrl(settings, workspaceId));
@@ -4757,8 +5230,23 @@ function firstPartyFilesMcpServerUrl(mcpUrl: string): string {
4757
5230
  return `${mcpUrl.replace(/\/+$/, "")}/files`;
4758
5231
  }
4759
5232
 
5233
+ const MODAL_DESKTOP_IMAGE_DIGEST_REF = /@sha256:[0-9a-f]{64}$/i;
5234
+
5235
+ function isDigestPinnedModalDesktopImage(settings: Settings): boolean {
5236
+ if (settings.modalImageId) return true;
5237
+ return (
5238
+ typeof settings.modalImageRef === "string" &&
5239
+ MODAL_DESKTOP_IMAGE_DIGEST_REF.test(settings.modalImageRef)
5240
+ );
5241
+ }
5242
+
4760
5243
  function validateSettings(settings: Settings): void {
4761
5244
  temporalConnectionOptions(settings);
5245
+ if (settings.goalIdleBackoffMs.some((delayMs) => delayMs > settings.goalIdleBackoffMaxMs)) {
5246
+ throw new Error(
5247
+ `OPENGENI_GOAL_IDLE_BACKOFF_MS entries must not exceed OPENGENI_GOAL_IDLE_BACKOFF_MAX_MS (${settings.goalIdleBackoffMaxMs})`,
5248
+ );
5249
+ }
4762
5250
  const allowedFirstPartyMcpTools = new Set(
4763
5251
  settings.allowedFirstPartyMcpTools ?? FIRST_PARTY_MCP_TOOL_NAMES,
4764
5252
  );
@@ -4847,6 +5335,59 @@ function validateSettings(settings: Settings): void {
4847
5335
  "OPENGENI_GOOGLE_DRIVE_CLIENT_ID and OPENGENI_GOOGLE_DRIVE_CLIENT_SECRET must be configured together",
4848
5336
  );
4849
5337
  }
5338
+ if (
5339
+ Boolean(settings.githubPersonalOauthClientId) !==
5340
+ Boolean(settings.githubPersonalOauthClientSecret)
5341
+ ) {
5342
+ throw new Error(
5343
+ "OPENGENI_GITHUB_PERSONAL_OAUTH_CLIENT_ID and OPENGENI_GITHUB_PERSONAL_OAUTH_CLIENT_SECRET must be configured together",
5344
+ );
5345
+ }
5346
+ if (settings.githubPersonalOauthEnabled) {
5347
+ if (!settings.integrationsEnabled) {
5348
+ throw new Error(
5349
+ "OPENGENI_INTEGRATIONS_ENABLED=true is required when personal GitHub OAuth is enabled",
5350
+ );
5351
+ }
5352
+ if (settings.productAccessMode !== "managed") {
5353
+ throw new Error(
5354
+ "OPENGENI_GITHUB_PERSONAL_OAUTH_ENABLED=true requires OPENGENI_PRODUCT_ACCESS_MODE=managed",
5355
+ );
5356
+ }
5357
+ if (!settings.githubPersonalOauthClientId || !settings.githubPersonalOauthClientSecret) {
5358
+ throw new Error(
5359
+ "personal GitHub OAuth requires OPENGENI_GITHUB_PERSONAL_OAUTH_CLIENT_ID and OPENGENI_GITHUB_PERSONAL_OAUTH_CLIENT_SECRET",
5360
+ );
5361
+ }
5362
+ if (settings.githubPersonalOauthClientId === settings.githubClientId) {
5363
+ throw new Error(
5364
+ "personal GitHub OAuth must use a different OAuth App client from the OpenGeni GitHub App",
5365
+ );
5366
+ }
5367
+ if (!personalGitHubOAuthCallbackUrl(settings.publicBaseUrl)) {
5368
+ throw new Error(
5369
+ "OPENGENI_PUBLIC_BASE_URL must be a credential-free origin without a path, query, or fragment when personal GitHub OAuth is enabled",
5370
+ );
5371
+ }
5372
+ if (
5373
+ !settings.publicBaseUrl?.startsWith("https://") &&
5374
+ !["local", "test"].includes(settings.environment)
5375
+ ) {
5376
+ throw new Error(
5377
+ "OPENGENI_PUBLIC_BASE_URL must use https when personal GitHub OAuth is enabled outside local/test",
5378
+ );
5379
+ }
5380
+ if (!settings.integrationsStateSecret) {
5381
+ throw new Error(
5382
+ "OPENGENI_INTEGRATIONS_STATE_SECRET is required when personal GitHub OAuth is enabled",
5383
+ );
5384
+ }
5385
+ if (!settings.environmentsEncryptionKey) {
5386
+ throw new Error(
5387
+ "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is required when personal GitHub OAuth is enabled",
5388
+ );
5389
+ }
5390
+ }
4850
5391
  if (Boolean(settings.fikenClientId) !== Boolean(settings.fikenClientSecret)) {
4851
5392
  throw new Error(
4852
5393
  "OPENGENI_FIKEN_OAUTH_CLIENT_ID and OPENGENI_FIKEN_OAUTH_CLIENT_SECRET must be configured together",
@@ -4873,11 +5414,21 @@ function validateSettings(settings: Settings): void {
4873
5414
  }
4874
5415
  }
4875
5416
  if (settings.googleDriveClientId) {
5417
+ if (!settings.integrationsEnabled) {
5418
+ throw new Error(
5419
+ "OPENGENI_INTEGRATIONS_ENABLED=true is required when the Google Drive integration is configured",
5420
+ );
5421
+ }
4876
5422
  if (!settings.publicBaseUrl) {
4877
5423
  throw new Error(
4878
5424
  "OPENGENI_PUBLIC_BASE_URL is required when the Google Drive integration is configured",
4879
5425
  );
4880
5426
  }
5427
+ if (!googleDriveOAuthCallbackUrl(settings.publicBaseUrl)) {
5428
+ throw new Error(
5429
+ "OPENGENI_PUBLIC_BASE_URL must be a credential-free origin without a path, query, or fragment when the Google Drive integration is configured",
5430
+ );
5431
+ }
4881
5432
  if (
4882
5433
  !settings.publicBaseUrl.startsWith("https://") &&
4883
5434
  !["local", "test"].includes(settings.environment)
@@ -4892,6 +5443,23 @@ function validateSettings(settings: Settings): void {
4892
5443
  );
4893
5444
  }
4894
5445
  }
5446
+ if (settings.googleDriveSyncMaxFileBytes > settings.googleDriveSyncMaxBytes) {
5447
+ throw new Error(
5448
+ "OPENGENI_GOOGLE_DRIVE_SYNC_MAX_FILE_BYTES must not exceed OPENGENI_GOOGLE_DRIVE_SYNC_MAX_BYTES",
5449
+ );
5450
+ }
5451
+ if (
5452
+ settings.googleDriveProviderRetryInitialDelayMs > settings.googleDriveProviderRetryMaxDelayMs
5453
+ ) {
5454
+ throw new Error(
5455
+ "OPENGENI_GOOGLE_DRIVE_PROVIDER_RETRY_INITIAL_DELAY_MS must not exceed OPENGENI_GOOGLE_DRIVE_PROVIDER_RETRY_MAX_DELAY_MS",
5456
+ );
5457
+ }
5458
+ if (settings.googleDriveProviderRetryInitialDelayMs > settings.googleDriveProviderRetryBudgetMs) {
5459
+ throw new Error(
5460
+ "OPENGENI_GOOGLE_DRIVE_PROVIDER_RETRY_INITIAL_DELAY_MS must not exceed OPENGENI_GOOGLE_DRIVE_PROVIDER_RETRY_BUDGET_MS",
5461
+ );
5462
+ }
4895
5463
  if (Boolean(settings.atlassianClientId) !== Boolean(settings.atlassianClientSecret)) {
4896
5464
  throw new Error(
4897
5465
  "OPENGENI_ATLASSIAN_CLIENT_ID and OPENGENI_ATLASSIAN_CLIENT_SECRET must be configured together",
@@ -5120,6 +5688,18 @@ function validateSettings(settings: Settings): void {
5120
5688
  sandboxLifecycleHookIds(settings);
5121
5689
  // Fail fast on a malformed warm-rate table (P2.1).
5122
5690
  parseSandboxWarmRateJson(settings.sandboxWarmRateMicrosPerSecondJson);
5691
+ if (settings.sandboxBackend === "opensandbox") {
5692
+ if (!/@sha256:[0-9a-f]{64}$/i.test(settings.openSandboxImage ?? "")) {
5693
+ throw new Error(
5694
+ "OPENGENI_OPENSANDBOX_IMAGE must be an immutable OCI reference ending in @sha256:<64 hex characters>",
5695
+ );
5696
+ }
5697
+ if (!objectStorageConfiguredForWorkspaceArchives(settings)) {
5698
+ throw new Error(
5699
+ "OPENGENI_SANDBOX_BACKEND=opensandbox requires configured object storage for portable /workspace archives",
5700
+ );
5701
+ }
5702
+ }
5123
5703
  const serverIds = new Set<string>();
5124
5704
  for (const server of settings.mcpServers) {
5125
5705
  if (serverIds.has(server.id)) {
@@ -5128,30 +5708,13 @@ function validateSettings(settings: Settings): void {
5128
5708
  serverIds.add(server.id);
5129
5709
  }
5130
5710
  // --- sandbox lease cadence invariant (fail fast at boot) ---
5131
- // reaperPeriod (30s) < viewerHolderTTL (90s), and reaperPeriod + idleGrace must
5132
- // be strictly less than the provider lifetime (modalTimeoutSeconds*1000):
5133
- // - the reaper must run more often than the TTL it polices; and
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).
5711
+ // Holder TTLs are provider-neutral. Modal's finite hard/idle clocks and
5712
+ // deadline rotation are validated only when Modal is active; renewable-TTL
5713
+ // providers such as OpenSandbox do not enter that deadline model.
5140
5714
  {
5141
5715
  const reaperPeriod = settings.sandboxLeaseReaperPeriodMs;
5142
5716
  const viewerTtl = settings.sandboxViewerHolderTtlMs;
5143
5717
  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
5718
  if (!(reaperPeriod < viewerTtl)) {
5156
5719
  throw new Error(
5157
5720
  `OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS (${reaperPeriod}) must be strictly less than ` +
@@ -5166,54 +5729,57 @@ function validateSettings(settings: Settings): void {
5166
5729
  `more often than the controller-heartbeat horizon.`,
5167
5730
  );
5168
5731
  }
5169
- if (!(idleTimeoutMs <= providerLifetimeMs)) {
5170
- throw new Error(
5171
- `OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS*1000 (${idleTimeoutMs}) must not exceed the hard provider ` +
5172
- `lifetime (OPENGENI_MODAL_TIMEOUT_SECONDS*1000 = ${providerLifetimeMs}): the idle timeout is a ` +
5173
- `floor under the hard lifetime, not above it.`,
5174
- );
5175
- }
5176
- if (!(rotationLeadMs < providerLifetimeMs)) {
5177
- throw new Error(
5178
- `OPENGENI_SANDBOX_ROTATION_LEAD_MS (${rotationLeadMs}) must be strictly less than ` +
5179
- `OPENGENI_MODAL_TIMEOUT_SECONDS*1000 (${providerLifetimeMs}).`,
5180
- );
5181
- }
5182
- // This is provider-hard-deadline headroom, not a retry delay. Rotation is
5183
- // admitted immediately before the same sweep's drain inventory, so only the
5184
- // worst-case time until that sweep plus the complete durable capture window
5185
- // is required. No second schedule period belongs in the availability path.
5186
- const captureTimeoutMs = sandboxArchiveCaptureTimeoutMs(settings);
5187
- if (!(rotationLeadMs > captureTimeoutMs + reaperPeriod)) {
5188
- throw new Error(
5189
- `OPENGENI_SANDBOX_ROTATION_LEAD_MS (${rotationLeadMs}) must exceed the durable capture ` +
5190
- `timeout plus one reaper period (${captureTimeoutMs + reaperPeriod}).`,
5191
- );
5192
- }
5193
- if (!(viewerTtl < idleTimeoutMs)) {
5194
- throw new Error(
5195
- `OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS (${viewerTtl}) must be strictly less than the effective box ` +
5196
- `idle timeout (${idleTimeoutMs}): a viewer holder must be reapable before the box idles out from ` +
5197
- `under it (the provider idle-timeout is the backstop).`,
5198
- );
5199
- }
5200
- if (!(interactionTtl < idleTimeoutMs)) {
5201
- throw new Error(
5202
- `OPENGENI_SANDBOX_INTERACTION_HOLDER_TTL_MS (${interactionTtl}) must be strictly less than ` +
5203
- `the effective box idle timeout (${idleTimeoutMs}): a dead browser controller must be ` +
5204
- `reapable before the provider reclaims its placement.`,
5205
- );
5206
- }
5207
- if (!(reaperPeriod + idleGraceMs < idleTimeoutMs)) {
5208
- throw new Error(
5209
- `OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS + OPENGENI_SANDBOX_IDLE_GRACE_MS ` +
5210
- `(${reaperPeriod} + ${idleGraceMs} = ${reaperPeriod + idleGraceMs}) must be strictly less than the ` +
5211
- `effective box idle timeout (${idleTimeoutMs}): a drained box must SURVIVE its full warm window so ` +
5212
- `the reaper can resume + snapshot /workspace + terminate it on the sweep AFTER the drain grace ` +
5213
- `elapses Modal's idle-reap must NOT fire first (or /workspace is lost). Raise ` +
5214
- `OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS (defaults to OPENGENI_MODAL_TIMEOUT_SECONDS) or lower ` +
5215
- `OPENGENI_SANDBOX_IDLE_GRACE_MS.`,
5216
- );
5732
+ if (settings.sandboxBackend === "modal") {
5733
+ const idleGraceMs = settings.sandboxIdleGraceMs;
5734
+ const lifecycle = effectiveSandboxLifecycle(settings, "modal");
5735
+ const providerLifetimeMs = lifecycle.hardLifetimeMs!;
5736
+ const rotationLeadMs = lifecycle.rotationLeadMs!;
5737
+ const idleTimeoutMs = lifecycle.providerIdleTimeoutMs!;
5738
+ if (!(idleTimeoutMs <= providerLifetimeMs)) {
5739
+ throw new Error(
5740
+ `OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS*1000 (${idleTimeoutMs}) must not exceed the hard provider ` +
5741
+ `lifetime (OPENGENI_MODAL_TIMEOUT_SECONDS*1000 = ${providerLifetimeMs}): the idle timeout is a ` +
5742
+ `floor under the hard lifetime, not above it.`,
5743
+ );
5744
+ }
5745
+ if (!(rotationLeadMs < providerLifetimeMs)) {
5746
+ throw new Error(
5747
+ `OPENGENI_SANDBOX_ROTATION_LEAD_MS (${rotationLeadMs}) must be strictly less than ` +
5748
+ `OPENGENI_MODAL_TIMEOUT_SECONDS*1000 (${providerLifetimeMs}).`,
5749
+ );
5750
+ }
5751
+ const captureTimeoutMs = sandboxArchiveCaptureTimeoutMs(settings);
5752
+ if (!(rotationLeadMs > captureTimeoutMs + reaperPeriod)) {
5753
+ throw new Error(
5754
+ `OPENGENI_SANDBOX_ROTATION_LEAD_MS (${rotationLeadMs}) must exceed the durable capture ` +
5755
+ `timeout plus one reaper period (${captureTimeoutMs + reaperPeriod}).`,
5756
+ );
5757
+ }
5758
+ if (!(viewerTtl < idleTimeoutMs)) {
5759
+ throw new Error(
5760
+ `OPENGENI_SANDBOX_VIEWER_HOLDER_TTL_MS (${viewerTtl}) must be strictly less than the effective box ` +
5761
+ `idle timeout (${idleTimeoutMs}): a viewer holder must be reapable before the box idles out from ` +
5762
+ `under it (the provider idle-timeout is the backstop).`,
5763
+ );
5764
+ }
5765
+ if (!(interactionTtl < idleTimeoutMs)) {
5766
+ throw new Error(
5767
+ `OPENGENI_SANDBOX_INTERACTION_HOLDER_TTL_MS (${interactionTtl}) must be strictly less than ` +
5768
+ `the effective box idle timeout (${idleTimeoutMs}): a dead browser controller must be ` +
5769
+ `reapable before the provider reclaims its placement.`,
5770
+ );
5771
+ }
5772
+ if (!(reaperPeriod + idleGraceMs < idleTimeoutMs)) {
5773
+ throw new Error(
5774
+ `OPENGENI_SANDBOX_LEASE_REAPER_PERIOD_MS + OPENGENI_SANDBOX_IDLE_GRACE_MS ` +
5775
+ `(${reaperPeriod} + ${idleGraceMs} = ${reaperPeriod + idleGraceMs}) must be strictly less than the ` +
5776
+ `effective box idle timeout (${idleTimeoutMs}): a drained box must SURVIVE its full warm window so ` +
5777
+ `the reaper can resume + snapshot /workspace + terminate it on the sweep AFTER the drain grace ` +
5778
+ `elapses — Modal's idle-reap must NOT fire first (or /workspace is lost). Raise ` +
5779
+ `OPENGENI_MODAL_IDLE_TIMEOUT_SECONDS (defaults to OPENGENI_MODAL_TIMEOUT_SECONDS) or lower ` +
5780
+ `OPENGENI_SANDBOX_IDLE_GRACE_MS.`,
5781
+ );
5782
+ }
5217
5783
  }
5218
5784
  }
5219
5785
  // --- stream-token secret: required-when-desktop, but GRACEFULLY DEGRADE (stream-token availability contract) ---
@@ -5224,6 +5790,20 @@ function validateSettings(settings: Settings): void {
5224
5790
  // negotiateCapabilities degrades the desktop cell). This keeps a desktop-
5225
5791
  // configured deployment bootable (headless + Channel-A still work) instead of
5226
5792
  // crashing the whole API on a missing secret.
5793
+ if (
5794
+ settings.sandboxDesktopEnabled &&
5795
+ settings.sandboxBackend === "modal" &&
5796
+ !["local", "test"].includes(settings.environment)
5797
+ ) {
5798
+ if (!isDigestPinnedModalDesktopImage(settings)) {
5799
+ throw new Error(
5800
+ "OPENGENI_MODAL_IMAGE_REF must be digest-pinned (registry/name@sha256:…) when " +
5801
+ "OPENGENI_SANDBOX_BACKEND=modal and OPENGENI_SANDBOX_DESKTOP_ENABLED=true. " +
5802
+ "Computer/Browser need docker/desktop.Dockerfile, not the official headless " +
5803
+ "opengeni-sandbox image. Helm desktop.imageRef writes this pin.",
5804
+ );
5805
+ }
5806
+ }
5227
5807
  if (settings.sandboxDesktopEnabled && resolveStreamTokenSecret(settings) === undefined) {
5228
5808
  console.warn(
5229
5809
  "[opengeni] OPENGENI_SANDBOX_DESKTOP_ENABLED=true but neither OPENGENI_STREAM_TOKEN_SECRET nor " +
@@ -5235,10 +5815,12 @@ function validateSettings(settings: Settings): void {
5235
5815
  // Model provider registry: parse it here so JSON/zod errors surface at boot,
5236
5816
  // reject a registry id colliding with the built-in provider id (it would
5237
5817
  // shadow the built-in in configuredProviders), reject duplicate registry
5238
- // ids, and require a resolvable API key for every registry provider (a
5239
- // provider with no usable key can never serve a turn). Registry models flow
5240
- // through configuredAllowedModels, so the managed-billing pricing check above
5241
- // already covers them.
5818
+ // ids, and preserve the existing key requirement for every registry provider
5819
+ // except connected Codex and the explicit anonymous opt-in. Anonymous
5820
+ // providers are externally metered; a missing key on every other ordinary
5821
+ // provider remains a boot error.
5822
+ // Registry models flow through configuredAllowedModels, so the managed-billing
5823
+ // pricing check above already covers the OpenGeni-credit providers.
5242
5824
  const registryProviders = parseModelProvidersJson(settings.modelProvidersJson);
5243
5825
  const builtinId = builtinProviderId(settings);
5244
5826
  const providerIds = new Set<string>();
@@ -5263,7 +5845,11 @@ function validateSettings(settings: Settings): void {
5263
5845
  );
5264
5846
  }
5265
5847
  providerIds.add(provider.id);
5266
- if (provider.kind !== "codex-subscription" && !resolveProviderApiKey(provider)) {
5848
+ if (
5849
+ provider.kind !== "codex-subscription" &&
5850
+ provider.kind !== "anonymous" &&
5851
+ !resolveProviderApiKey(provider)
5852
+ ) {
5267
5853
  throw new Error(
5268
5854
  `OPENGENI_MODEL_PROVIDERS_JSON provider ${provider.id} requires a resolvable API key (set apiKey or apiKeyEnv)`,
5269
5855
  );
@@ -5373,7 +5959,7 @@ export function resolveNatsCalloutConfig(settings: Settings): NatsCalloutConfig
5373
5959
  * The PRIVILEGED control-plane NATS login (api/worker). Present only when BOTH a
5374
5960
  * user and password are set; otherwise null and the bus connects anonymously (local
5375
5961
  * dev / a NATS without auth_callout). When the callout plane is on, this is the
5376
- * static account user permitted to request `agent.*.rpc`.
5962
+ * static account user permitted to request exact generation-fenced agent RPC subjects.
5377
5963
  */
5378
5964
  export interface NatsControlPlaneAuth {
5379
5965
  user: string;