@opengeni/config 0.2.4 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/config",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "OpenGeni runtime configuration: settings resolution, deployment knobs, and config validation shared across the server packages.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -32,15 +32,12 @@
32
32
  },
33
33
  "scripts": {
34
34
  "typecheck": "tsc --noEmit",
35
- "build": "tsup"
35
+ "build": "tsup",
36
+ "prepublishOnly": "bash ../../scripts/prepublish-guard"
36
37
  },
37
38
  "dependencies": {
38
39
  "@opengeni/codex": "^0.2.1",
39
- "@opengeni/contracts": "^0.6.0",
40
+ "@opengeni/contracts": "^0.9.0",
40
41
  "zod": "^4.2.1"
41
- },
42
- "devDependencies": {
43
- "tsup": "^8.5.0",
44
- "typescript": "^6.0.3"
45
42
  }
46
43
  }
package/src/index.ts CHANGED
@@ -101,10 +101,23 @@ export const DEFAULT_AGENT_INSTRUCTIONS = [
101
101
  AGENT_INSTRUCTIONS_CORE_PLACEHOLDER,
102
102
  ].join(" ");
103
103
 
104
+ export const McpServerConnectionRefSchema = z.object({
105
+ connectionId: z.string().uuid().optional(),
106
+ providerDomain: z.string().min(1),
107
+ kind: z.enum(["oauth2", "api_key", "app_install", "delegated"]).optional(),
108
+ scopes: z.array(z.string().min(1)).optional(),
109
+ resource: z.string().min(1).optional(),
110
+ subjectScope: z.enum(["workspace", "subject"]).optional(),
111
+ }).strict();
112
+ export type McpServerConnectionRef = z.infer<typeof McpServerConnectionRefSchema>;
113
+
104
114
  const SettingsSchema = z.object({
105
115
  serviceName: z.string().default("opengeni"),
106
116
  environment: z.string().default("local"),
107
117
  deploymentRevision: z.string().default("dev"),
118
+ // The release-train version baked into official images (OPENGENI_SERVER_VERSION).
119
+ // Absent on dev/source builds — consumers must treat it as optional.
120
+ serverVersion: z.string().optional(),
108
121
  databaseUrl: z.string().default("postgres://opengeni:opengeni@127.0.0.1:5432/opengeni"),
109
122
  // Step I (§7.8 runtime half). Dedicated Postgres schema for the EMBEDDED
110
123
  // topology. Default "" → standalone: no search_path scoping, server default
@@ -149,7 +162,13 @@ const SettingsSchema = z.object({
149
162
  // holder of stream:control gets 403 until this flips. Keeps stream:control a
150
163
  // declared-but-inert permission so later hardening is a flag flip.
151
164
  streamControlEnabled: EnvBoolean.default(false),
165
+ toolspaceEnabled: EnvBoolean.default(false),
166
+ toolspaceMaxCallsPerTurn: z.coerce.number().int().positive().default(200),
152
167
  environmentsEncryptionKey: z.string().optional(),
168
+ integrationsEnabled: EnvBoolean.default(false),
169
+ integrationsStateSecret: z.string().optional(),
170
+ integrationsAllowPrivateNetworkTargets: EnvBoolean.default(false),
171
+ integrationsOauthClientsJson: z.string().default("{}"),
153
172
  // Session goal guard rails. Goals are designed for runs that legitimately
154
173
  // span days, so length is bounded by pathology detection (no-progress
155
174
  // streaks, budget exhaustion), never by count. goalMaxAutoContinuations is
@@ -186,15 +205,26 @@ const SettingsSchema = z.object({
186
205
  // derived from these settings on the server path, and use the same numbers to
187
206
  // budget the client path.
188
207
  contextWindowTokens: z.coerce.number().int().positive().default(1_050_000),
208
+ // Proactive compaction threshold as a ratio of the model context window.
209
+ // Defaults to 60% and is clamped to [0.3, 0.9] so deployments can tune the
210
+ // trigger without accidentally disabling compaction or waiting until the
211
+ // provider is already at the cliff.
212
+ contextCompactionThresholdRatio: z.coerce.number().default(0.6).transform((value) => {
213
+ if (!Number.isFinite(value)) {
214
+ return 0.6;
215
+ }
216
+ return Math.min(0.9, Math.max(0.3, value));
217
+ }),
189
218
  // Tokens reserved for model output; subtracted from the window to get the
190
219
  // usable input budget B = contextWindowTokens - contextReservedOutputTokens.
191
220
  contextReservedOutputTokens: z.coerce.number().int().nonnegative().default(128_000),
192
221
  // Server path only: explicit compact_threshold (tokens) handed to the SDK's
193
- // StaticCompactionPolicy. Defaults to floor(B * contextCompactSoftFraction)
194
- // when unset.
222
+ // StaticCompactionPolicy. Defaults to floor(contextWindowTokens *
223
+ // contextCompactionThresholdRatio) when unset.
195
224
  contextServerCompactThresholdTokens: z.coerce.number().int().positive().optional(),
196
- // Server path/back-compat knobs. The client compaction path ignores these:
197
- // it uses Codex-parity 0.9 * (window - reserved output - 20k summary buffer).
225
+ // Deprecated back-compat knobs. The threshold is now controlled by
226
+ // contextCompactionThresholdRatio; these remain parsed so older deployments do
227
+ // not fail boot when their env still contains them.
198
228
  contextCompactSoftFraction: z.coerce.number().positive().max(1).default(0.70),
199
229
  contextCompactHardFraction: z.coerce.number().positive().max(1).default(0.85),
200
230
  // Deprecated for the client path; parsed for env/back-compat only.
@@ -208,6 +238,7 @@ const SettingsSchema = z.object({
208
238
  authAllowMetrics: EnvBoolean.default(false),
209
239
  apiHost: z.string().default("0.0.0.0"),
210
240
  apiPort: z.coerce.number().int().positive().default(8000),
241
+ workerHttpPort: z.coerce.number().int().positive().default(8001),
211
242
  opengeniMcpUrl: z.string().url().optional(),
212
243
  corsAllowOriginRegex: z.string().default(String.raw`^https?://(localhost|127\.0\.0\.1)(:\d+)?$`),
213
244
  openaiProvider: z.enum(["openai", "azure"]).default("openai"),
@@ -580,6 +611,14 @@ const SettingsSchema = z.object({
580
611
  allowedTools: z.array(z.string().min(1)).optional(),
581
612
  timeoutMs: z.number().int().positive().optional(),
582
613
  cacheToolsList: z.boolean().default(false),
614
+ /**
615
+ * Human-approval policy for this server's tools, overlaid per-run from a
616
+ * session MCP server row (never from OPENGENI_MCP_SERVERS). `true` = all
617
+ * tools require approval; a string[] = only the listed UNPREFIXED tool
618
+ * names do; absent = auto-run (the historical default). Enforced in the
619
+ * runtime by attaching `needsApproval` to the matching MCP tools.
620
+ */
621
+ requireApproval: z.union([z.boolean(), z.array(z.string().min(1))]).optional(),
583
622
  /**
584
623
  * Extra request headers sent to this MCP server (credential injection
585
624
  * for workspace-enabled capability MCPs). Populated at runtime from
@@ -587,6 +626,7 @@ const SettingsSchema = z.object({
587
626
  * OPENGENI_MCP_SERVERS.
588
627
  */
589
628
  headers: z.record(z.string(), z.string()).optional(),
629
+ connectionRef: McpServerConnectionRefSchema.optional(),
590
630
  })).default([]),
591
631
  });
592
632
 
@@ -659,6 +699,13 @@ const RegistryProviderSchema = z.object({
659
699
  });
660
700
  export type RegistryProvider = z.infer<typeof RegistryProviderSchema>;
661
701
 
702
+ export const IntegrationOAuthClientConfigSchema = z.object({
703
+ clientId: z.string().min(1),
704
+ clientSecret: z.string().min(1).optional(),
705
+ tokenEndpointAuthMethod: z.enum(["none", "client_secret_post", "client_secret_basic"]).default("none"),
706
+ });
707
+ export type IntegrationOAuthClientConfig = z.infer<typeof IntegrationOAuthClientConfigSchema>;
708
+
662
709
  /**
663
710
  * Runtime-resolved provider (built-in or registry), client-construction-ready.
664
711
  * The built-in OpenAI/Azure provider is always present and always "responses";
@@ -834,6 +881,7 @@ export function getSettings(): Settings {
834
881
  serviceName: optional("OPENGENI_SERVICE_NAME"),
835
882
  environment: optional("OPENGENI_ENVIRONMENT"),
836
883
  deploymentRevision: optional("OPENGENI_DEPLOYMENT_REVISION") ?? optional("SOURCE_VERSION") ?? optional("GITHUB_SHA"),
884
+ serverVersion: optional("OPENGENI_SERVER_VERSION"),
837
885
  databaseUrl: optional("OPENGENI_DATABASE_URL"),
838
886
  dbSchema: optional("OPENGENI_DB_SCHEMA"),
839
887
  rlsStrategy: optional("OPENGENI_RLS_STRATEGY"),
@@ -859,13 +907,20 @@ export function getSettings(): Settings {
859
907
  delegationSecret: optional("OPENGENI_DELEGATION_SECRET"),
860
908
  streamTokenSecret: optional("OPENGENI_STREAM_TOKEN_SECRET"),
861
909
  streamControlEnabled: optional("OPENGENI_STREAM_CONTROL_ENABLED"),
910
+ toolspaceEnabled: optional("OPENGENI_TOOLSPACE_ENABLED"),
911
+ toolspaceMaxCallsPerTurn: optional("OPENGENI_TOOLSPACE_MAX_CALLS_PER_TURN"),
862
912
  environmentsEncryptionKey: optional("OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY"),
913
+ integrationsEnabled: optional("OPENGENI_INTEGRATIONS_ENABLED"),
914
+ integrationsStateSecret: optional("OPENGENI_INTEGRATIONS_STATE_SECRET"),
915
+ integrationsAllowPrivateNetworkTargets: optional("OPENGENI_INTEGRATIONS_ALLOW_PRIVATE_NETWORK_TARGETS"),
916
+ integrationsOauthClientsJson: optional("OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON"),
863
917
  goalMaxAutoContinuations: optional("OPENGENI_GOAL_MAX_AUTO_CONTINUATIONS"),
864
918
  goalNoProgressLimit: optional("OPENGENI_GOAL_NO_PROGRESS_LIMIT"),
865
919
  agentMaxModelCallsPerTurn: optional("OPENGENI_AGENT_MAX_MODEL_CALLS_PER_TURN"),
866
920
  sessionHistorySource: optional("OPENGENI_SESSION_HISTORY_SOURCE"),
867
921
  contextCompactionMode: optional("OPENGENI_CONTEXT_COMPACTION_MODE"),
868
922
  contextWindowTokens: optional("OPENGENI_CONTEXT_WINDOW_TOKENS"),
923
+ contextCompactionThresholdRatio: optional("OPENGENI_COMPACTION_THRESHOLD_RATIO"),
869
924
  contextReservedOutputTokens: optional("OPENGENI_CONTEXT_RESERVED_OUTPUT_TOKENS"),
870
925
  contextServerCompactThresholdTokens: optional("OPENGENI_CONTEXT_SERVER_COMPACT_THRESHOLD_TOKENS"),
871
926
  contextCompactSoftFraction: optional("OPENGENI_CONTEXT_COMPACT_SOFT_FRACTION"),
@@ -878,6 +933,7 @@ export function getSettings(): Settings {
878
933
  authAllowMetrics: optional("OPENGENI_AUTH_ALLOW_METRICS"),
879
934
  apiHost: optional("OPENGENI_API_HOST"),
880
935
  apiPort: optional("OPENGENI_API_PORT"),
936
+ workerHttpPort: optional("OPENGENI_WORKER_HTTP_PORT"),
881
937
  opengeniMcpUrl: optional("OPENGENI_MCP_URL"),
882
938
  corsAllowOriginRegex: optional("OPENGENI_CORS_ALLOW_ORIGIN_REGEX"),
883
939
  openaiProvider: optional("OPENGENI_OPENAI_PROVIDER"),
@@ -1304,11 +1360,11 @@ export function contextInputBudgetTokens(settings: Pick<Settings, "contextWindow
1304
1360
  * floor(B * softFraction). This is what sidesteps the SDK's wrong 240k
1305
1361
  * fallback for gpt-5.5 (which is absent from its hardcoded window map).
1306
1362
  */
1307
- export function contextServerCompactThreshold(settings: Pick<Settings, "contextWindowTokens" | "contextReservedOutputTokens" | "contextServerCompactThresholdTokens" | "contextCompactSoftFraction">): number {
1363
+ export function contextServerCompactThreshold(settings: Pick<Settings, "contextWindowTokens" | "contextReservedOutputTokens" | "contextServerCompactThresholdTokens" | "contextCompactSoftFraction" | "contextCompactionThresholdRatio">): number {
1308
1364
  if (settings.contextServerCompactThresholdTokens) {
1309
1365
  return settings.contextServerCompactThresholdTokens;
1310
1366
  }
1311
- return Math.floor(contextInputBudgetTokens(settings) * settings.contextCompactSoftFraction);
1367
+ return Math.floor(settings.contextWindowTokens * settings.contextCompactionThresholdRatio);
1312
1368
  }
1313
1369
 
1314
1370
  export function configuredStaticUsageLimits(settings: Settings): StaticUsageLimitsConfig {
@@ -1428,6 +1484,7 @@ export function collectGitIdentityEnvironment(settings: Settings): Record<string
1428
1484
  export function stableSandboxEnvironmentForRun(
1429
1485
  settings: Settings,
1430
1486
  workspaceEnvironment: Record<string, string> = {},
1487
+ options: { workspaceId?: string } = {},
1431
1488
  ): Record<string, string> {
1432
1489
  const environment: Record<string, string> = {
1433
1490
  ...collectSandboxEnvironment(settings),
@@ -1449,6 +1506,12 @@ export function stableSandboxEnvironmentForRun(
1449
1506
  // VALUE lives exclusively in the file (agent-managed, refreshable mid-turn), never
1450
1507
  // the manifest env.
1451
1508
  environment.OPENGENI_GIT_TOKEN_FILE ??= `${environment.HOME ?? descriptor.workspaceRoot}/.opengeni/git-token`;
1509
+ if (settings.toolspaceEnabled) {
1510
+ environment.OPENGENI_TOOLSPACE_TOKEN_FILE ??= `${environment.HOME ?? descriptor.workspaceRoot}/.opengeni/toolspace-token`;
1511
+ if (options.workspaceId) {
1512
+ environment.OPENGENI_TOOLSPACE_URL ??= firstPartyMcpWorkspaceUrl(settings, options.workspaceId);
1513
+ }
1514
+ }
1452
1515
  return environment;
1453
1516
  }
1454
1517
 
@@ -1693,6 +1756,34 @@ export function parseModelProvidersJson(raw: string): RegistryProvider[] {
1693
1756
  });
1694
1757
  }
1695
1758
 
1759
+ export function parseIntegrationsOauthClientsJson(raw: string | undefined): Record<string, IntegrationOAuthClientConfig> {
1760
+ if (!raw?.trim() || raw.trim() === "{}") {
1761
+ return {};
1762
+ }
1763
+ let parsed: unknown;
1764
+ try {
1765
+ parsed = JSON.parse(raw);
1766
+ } catch (error) {
1767
+ const message = error instanceof Error ? error.message : String(error);
1768
+ throw new Error(`OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON must be valid JSON: ${message}`);
1769
+ }
1770
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1771
+ throw new Error("OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON must be a JSON object keyed by authorization-server issuer or URL");
1772
+ }
1773
+ const out: Record<string, IntegrationOAuthClientConfig> = {};
1774
+ for (const [key, value] of Object.entries(parsed)) {
1775
+ if (!key.trim()) {
1776
+ throw new Error("OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON contains an empty issuer key");
1777
+ }
1778
+ const result = IntegrationOAuthClientConfigSchema.safeParse(value);
1779
+ if (!result.success) {
1780
+ throw new Error(`OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON client for ${key} is invalid: ${result.error.message}`);
1781
+ }
1782
+ out[key] = result.data;
1783
+ }
1784
+ return out;
1785
+ }
1786
+
1696
1787
  export function parseStaticUsageLimitsJson(raw: string): StaticUsageLimitsConfig {
1697
1788
  if (!raw.trim() || raw.trim() === "{}") {
1698
1789
  return {};
@@ -1786,7 +1877,7 @@ function ensureBuiltInMcpServers(settings: Settings): Settings["mcpServers"] {
1786
1877
  id: "docs",
1787
1878
  name: "Document Search",
1788
1879
  url: firstPartyDocsMcpUrl,
1789
- allowedTools: ["search_documents", "fetch_document_chunk", "list_document_bases"],
1880
+ allowedTools: ["search_documents", "fetch_document_chunk", "list_document_bases", "knowledge_search", "knowledge_fetch", "memory_search", "memory_propose"],
1790
1881
  cacheToolsList: false,
1791
1882
  }]),
1792
1883
  ...existing,
@@ -1818,6 +1909,18 @@ export function firstPartyMcpBaseUrl(settings: Settings): string {
1818
1909
  return settings.opengeniMcpUrl ?? `http://127.0.0.1:${settings.apiPort}/v1/workspaces/{workspaceId}/mcp`;
1819
1910
  }
1820
1911
 
1912
+ export function firstPartyMcpWorkspaceUrl(settings: Settings, workspaceId: string): string {
1913
+ const raw = firstPartyMcpBaseUrl(settings);
1914
+ if (raw.includes("{workspaceId}")) {
1915
+ return raw.replaceAll("{workspaceId}", workspaceId);
1916
+ }
1917
+ const url = new URL(raw);
1918
+ url.pathname = `/v1/workspaces/${workspaceId}/mcp`;
1919
+ url.search = "";
1920
+ url.hash = "";
1921
+ return url.toString();
1922
+ }
1923
+
1821
1924
  function firstPartyMcpServerUrl(settings: Settings): string {
1822
1925
  return firstPartyMcpBaseUrl(settings);
1823
1926
  }
@@ -1827,6 +1930,9 @@ function firstPartyDocumentsMcpServerUrl(mcpUrl: string): string {
1827
1930
  }
1828
1931
 
1829
1932
  function validateSettings(settings: Settings): void {
1933
+ if (settings.toolspaceEnabled && !settings.delegationSecret) {
1934
+ throw new Error("OPENGENI_DELEGATION_SECRET is required when OPENGENI_TOOLSPACE_ENABLED=true");
1935
+ }
1830
1936
  if (settings.productAccessMode === "managed") {
1831
1937
  if (!settings.publicBaseUrl) {
1832
1938
  throw new Error("OPENGENI_PUBLIC_BASE_URL is required when OPENGENI_PRODUCT_ACCESS_MODE=managed");
@@ -1845,6 +1951,18 @@ function validateSettings(settings: Settings): void {
1845
1951
  }
1846
1952
  }
1847
1953
  environmentsEncryptionKeyBytes(settings);
1954
+ if (settings.integrationsEnabled) {
1955
+ if (settings.productAccessMode === "managed" && !settings.publicBaseUrl) {
1956
+ throw new Error("OPENGENI_PUBLIC_BASE_URL is required when OPENGENI_INTEGRATIONS_ENABLED=true and OPENGENI_PRODUCT_ACCESS_MODE=managed");
1957
+ }
1958
+ if (settings.publicBaseUrl && !settings.publicBaseUrl.startsWith("https://") && !["local", "test"].includes(settings.environment)) {
1959
+ throw new Error("OPENGENI_PUBLIC_BASE_URL must use https when OPENGENI_INTEGRATIONS_ENABLED=true outside local/test");
1960
+ }
1961
+ if (!settings.integrationsStateSecret && !["local", "test"].includes(settings.environment)) {
1962
+ throw new Error("OPENGENI_INTEGRATIONS_STATE_SECRET is required when OPENGENI_INTEGRATIONS_ENABLED=true outside local/test");
1963
+ }
1964
+ }
1965
+ parseIntegrationsOauthClientsJson(settings.integrationsOauthClientsJson);
1848
1966
  if (
1849
1967
  settings.productAccessMode === "configured"
1850
1968
  && !["local", "test"].includes(settings.environment)