@opengeni/config 0.2.5 → 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.5",
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.7.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,6 +101,16 @@ 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"),
@@ -152,7 +162,13 @@ const SettingsSchema = z.object({
152
162
  // holder of stream:control gets 403 until this flips. Keeps stream:control a
153
163
  // declared-but-inert permission so later hardening is a flag flip.
154
164
  streamControlEnabled: EnvBoolean.default(false),
165
+ toolspaceEnabled: EnvBoolean.default(false),
166
+ toolspaceMaxCallsPerTurn: z.coerce.number().int().positive().default(200),
155
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("{}"),
156
172
  // Session goal guard rails. Goals are designed for runs that legitimately
157
173
  // span days, so length is bounded by pathology detection (no-progress
158
174
  // streaks, budget exhaustion), never by count. goalMaxAutoContinuations is
@@ -189,15 +205,26 @@ const SettingsSchema = z.object({
189
205
  // derived from these settings on the server path, and use the same numbers to
190
206
  // budget the client path.
191
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
+ }),
192
218
  // Tokens reserved for model output; subtracted from the window to get the
193
219
  // usable input budget B = contextWindowTokens - contextReservedOutputTokens.
194
220
  contextReservedOutputTokens: z.coerce.number().int().nonnegative().default(128_000),
195
221
  // Server path only: explicit compact_threshold (tokens) handed to the SDK's
196
- // StaticCompactionPolicy. Defaults to floor(B * contextCompactSoftFraction)
197
- // when unset.
222
+ // StaticCompactionPolicy. Defaults to floor(contextWindowTokens *
223
+ // contextCompactionThresholdRatio) when unset.
198
224
  contextServerCompactThresholdTokens: z.coerce.number().int().positive().optional(),
199
- // Server path/back-compat knobs. The client compaction path ignores these:
200
- // 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.
201
228
  contextCompactSoftFraction: z.coerce.number().positive().max(1).default(0.70),
202
229
  contextCompactHardFraction: z.coerce.number().positive().max(1).default(0.85),
203
230
  // Deprecated for the client path; parsed for env/back-compat only.
@@ -584,6 +611,14 @@ const SettingsSchema = z.object({
584
611
  allowedTools: z.array(z.string().min(1)).optional(),
585
612
  timeoutMs: z.number().int().positive().optional(),
586
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(),
587
622
  /**
588
623
  * Extra request headers sent to this MCP server (credential injection
589
624
  * for workspace-enabled capability MCPs). Populated at runtime from
@@ -591,6 +626,7 @@ const SettingsSchema = z.object({
591
626
  * OPENGENI_MCP_SERVERS.
592
627
  */
593
628
  headers: z.record(z.string(), z.string()).optional(),
629
+ connectionRef: McpServerConnectionRefSchema.optional(),
594
630
  })).default([]),
595
631
  });
596
632
 
@@ -663,6 +699,13 @@ const RegistryProviderSchema = z.object({
663
699
  });
664
700
  export type RegistryProvider = z.infer<typeof RegistryProviderSchema>;
665
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
+
666
709
  /**
667
710
  * Runtime-resolved provider (built-in or registry), client-construction-ready.
668
711
  * The built-in OpenAI/Azure provider is always present and always "responses";
@@ -864,13 +907,20 @@ export function getSettings(): Settings {
864
907
  delegationSecret: optional("OPENGENI_DELEGATION_SECRET"),
865
908
  streamTokenSecret: optional("OPENGENI_STREAM_TOKEN_SECRET"),
866
909
  streamControlEnabled: optional("OPENGENI_STREAM_CONTROL_ENABLED"),
910
+ toolspaceEnabled: optional("OPENGENI_TOOLSPACE_ENABLED"),
911
+ toolspaceMaxCallsPerTurn: optional("OPENGENI_TOOLSPACE_MAX_CALLS_PER_TURN"),
867
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"),
868
917
  goalMaxAutoContinuations: optional("OPENGENI_GOAL_MAX_AUTO_CONTINUATIONS"),
869
918
  goalNoProgressLimit: optional("OPENGENI_GOAL_NO_PROGRESS_LIMIT"),
870
919
  agentMaxModelCallsPerTurn: optional("OPENGENI_AGENT_MAX_MODEL_CALLS_PER_TURN"),
871
920
  sessionHistorySource: optional("OPENGENI_SESSION_HISTORY_SOURCE"),
872
921
  contextCompactionMode: optional("OPENGENI_CONTEXT_COMPACTION_MODE"),
873
922
  contextWindowTokens: optional("OPENGENI_CONTEXT_WINDOW_TOKENS"),
923
+ contextCompactionThresholdRatio: optional("OPENGENI_COMPACTION_THRESHOLD_RATIO"),
874
924
  contextReservedOutputTokens: optional("OPENGENI_CONTEXT_RESERVED_OUTPUT_TOKENS"),
875
925
  contextServerCompactThresholdTokens: optional("OPENGENI_CONTEXT_SERVER_COMPACT_THRESHOLD_TOKENS"),
876
926
  contextCompactSoftFraction: optional("OPENGENI_CONTEXT_COMPACT_SOFT_FRACTION"),
@@ -1310,11 +1360,11 @@ export function contextInputBudgetTokens(settings: Pick<Settings, "contextWindow
1310
1360
  * floor(B * softFraction). This is what sidesteps the SDK's wrong 240k
1311
1361
  * fallback for gpt-5.5 (which is absent from its hardcoded window map).
1312
1362
  */
1313
- export function contextServerCompactThreshold(settings: Pick<Settings, "contextWindowTokens" | "contextReservedOutputTokens" | "contextServerCompactThresholdTokens" | "contextCompactSoftFraction">): number {
1363
+ export function contextServerCompactThreshold(settings: Pick<Settings, "contextWindowTokens" | "contextReservedOutputTokens" | "contextServerCompactThresholdTokens" | "contextCompactSoftFraction" | "contextCompactionThresholdRatio">): number {
1314
1364
  if (settings.contextServerCompactThresholdTokens) {
1315
1365
  return settings.contextServerCompactThresholdTokens;
1316
1366
  }
1317
- return Math.floor(contextInputBudgetTokens(settings) * settings.contextCompactSoftFraction);
1367
+ return Math.floor(settings.contextWindowTokens * settings.contextCompactionThresholdRatio);
1318
1368
  }
1319
1369
 
1320
1370
  export function configuredStaticUsageLimits(settings: Settings): StaticUsageLimitsConfig {
@@ -1434,6 +1484,7 @@ export function collectGitIdentityEnvironment(settings: Settings): Record<string
1434
1484
  export function stableSandboxEnvironmentForRun(
1435
1485
  settings: Settings,
1436
1486
  workspaceEnvironment: Record<string, string> = {},
1487
+ options: { workspaceId?: string } = {},
1437
1488
  ): Record<string, string> {
1438
1489
  const environment: Record<string, string> = {
1439
1490
  ...collectSandboxEnvironment(settings),
@@ -1455,6 +1506,12 @@ export function stableSandboxEnvironmentForRun(
1455
1506
  // VALUE lives exclusively in the file (agent-managed, refreshable mid-turn), never
1456
1507
  // the manifest env.
1457
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
+ }
1458
1515
  return environment;
1459
1516
  }
1460
1517
 
@@ -1699,6 +1756,34 @@ export function parseModelProvidersJson(raw: string): RegistryProvider[] {
1699
1756
  });
1700
1757
  }
1701
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
+
1702
1787
  export function parseStaticUsageLimitsJson(raw: string): StaticUsageLimitsConfig {
1703
1788
  if (!raw.trim() || raw.trim() === "{}") {
1704
1789
  return {};
@@ -1792,7 +1877,7 @@ function ensureBuiltInMcpServers(settings: Settings): Settings["mcpServers"] {
1792
1877
  id: "docs",
1793
1878
  name: "Document Search",
1794
1879
  url: firstPartyDocsMcpUrl,
1795
- 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"],
1796
1881
  cacheToolsList: false,
1797
1882
  }]),
1798
1883
  ...existing,
@@ -1824,6 +1909,18 @@ export function firstPartyMcpBaseUrl(settings: Settings): string {
1824
1909
  return settings.opengeniMcpUrl ?? `http://127.0.0.1:${settings.apiPort}/v1/workspaces/{workspaceId}/mcp`;
1825
1910
  }
1826
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
+
1827
1924
  function firstPartyMcpServerUrl(settings: Settings): string {
1828
1925
  return firstPartyMcpBaseUrl(settings);
1829
1926
  }
@@ -1833,6 +1930,9 @@ function firstPartyDocumentsMcpServerUrl(mcpUrl: string): string {
1833
1930
  }
1834
1931
 
1835
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
+ }
1836
1936
  if (settings.productAccessMode === "managed") {
1837
1937
  if (!settings.publicBaseUrl) {
1838
1938
  throw new Error("OPENGENI_PUBLIC_BASE_URL is required when OPENGENI_PRODUCT_ACCESS_MODE=managed");
@@ -1851,6 +1951,18 @@ function validateSettings(settings: Settings): void {
1851
1951
  }
1852
1952
  }
1853
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);
1854
1966
  if (
1855
1967
  settings.productAccessMode === "configured"
1856
1968
  && !["local", "test"].includes(settings.environment)