@opengeni/config 0.5.0 → 0.6.2

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.5.0",
3
+ "version": "0.6.2",
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": {
@@ -33,8 +33,8 @@
33
33
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
34
34
  },
35
35
  "dependencies": {
36
- "@opengeni/codex": "^0.2.2",
37
- "@opengeni/contracts": "^0.10.0",
36
+ "@opengeni/codex": "^0.2.5",
37
+ "@opengeni/contracts": "^0.15.0",
38
38
  "zod": "^4.2.1"
39
39
  },
40
40
  "engines": {
package/src/index.ts CHANGED
@@ -90,7 +90,7 @@ export const DEFAULT_AGENT_INSTRUCTIONS = [
90
90
  "You are an OpenGeni workspace agent.",
91
91
  "Follow the user's task and any enabled pack or skill instructions for the current role.",
92
92
  "Work inside the sandbox workspace and use filesystem and shell tools when useful.",
93
- "Repository resources are mounted under repos/<owner>/<repo>.",
93
+ "Repository resources are mounted under repos/<host>/<owner>/<repo> unless the session specifies another collision-free mount path.",
94
94
  "File resources are mounted under files/<file-id>/ unless the session specifies another mount path.",
95
95
  "Attached files are mounted read-only; copy them before modifying.",
96
96
  "Bundled skills are under .agents/ and can include infrastructure, marketing, or other role-specific guidance.",
@@ -103,14 +103,59 @@ export const DEFAULT_AGENT_INSTRUCTIONS = [
103
103
 
104
104
  export const McpServerConnectionRefSchema = z
105
105
  .object({
106
- connectionId: z.string().uuid().optional(),
106
+ // Standalone ids are UUIDs; embedded hosts may use any stable opaque id.
107
+ connectionId: z.string().min(1).optional(),
108
+ provider: z.string().min(1).max(128).optional(),
107
109
  providerDomain: z.string().min(1),
108
110
  kind: z.enum(["oauth2", "api_key", "app_install", "delegated"]).optional(),
109
111
  scopes: z.array(z.string().min(1)).optional(),
110
112
  resource: z.string().min(1).optional(),
113
+ selectedResources: z
114
+ .array(
115
+ z
116
+ .object({
117
+ id: z.string().min(1).max(512),
118
+ kind: z.literal("repository"),
119
+ })
120
+ .strict(),
121
+ )
122
+ .min(1)
123
+ .max(256)
124
+ .superRefine((resources, context) => {
125
+ const seen = new Set<string>();
126
+ for (const [index, resource] of resources.entries()) {
127
+ const key = `${resource.kind}\0${resource.id}`;
128
+ if (seen.has(key)) {
129
+ context.addIssue({
130
+ code: "custom",
131
+ message: "selectedResources must not contain duplicates",
132
+ path: [index],
133
+ });
134
+ }
135
+ seen.add(key);
136
+ }
137
+ })
138
+ .optional(),
111
139
  subjectScope: z.enum(["workspace", "subject"]).optional(),
112
140
  })
113
- .strict();
141
+ .strict()
142
+ .superRefine((reference, context) => {
143
+ if (!reference.selectedResources) return;
144
+ if (!reference.connectionId) {
145
+ context.addIssue({
146
+ code: "custom",
147
+ message: "selectedResources requires connectionId",
148
+ path: ["connectionId"],
149
+ });
150
+ }
151
+ if (!reference.provider) {
152
+ context.addIssue({
153
+ code: "custom",
154
+ message: "selectedResources requires provider",
155
+ path: ["provider"],
156
+ });
157
+ }
158
+ });
114
159
  export type McpServerConnectionRef = z.infer<typeof McpServerConnectionRefSchema>;
115
160
 
116
161
  const SettingsSchema = z.object({
@@ -125,7 +170,7 @@ const SettingsSchema = z.object({
125
170
  // topology. Default "" → standalone: no search_path scoping, server default
126
171
  // (`public`). When set (e.g. "opengeni"), the db handle + the managed-auth
127
172
  // pool send `search_path = "<dbSchema>","opengeni_private","public"` so every
128
- // query resolves into the dedicated schema with NO query rewrite (SPIKE-1 F1).
173
+ // query resolves into the dedicated schema with NO query rewrite (schema-isolation contract F1).
129
174
  dbSchema: z.string().default(""),
130
175
  // Step I (§7.7). RLS posture. "force" (default) = today's FORCE-RLS via the
131
176
  // non-owner `opengeni_app` role. "scoped" = the embedded owner-role path (the
@@ -135,6 +180,12 @@ const SettingsSchema = z.object({
135
180
  temporalHost: z.string().default("127.0.0.1:7233"),
136
181
  temporalNamespace: z.string().default("default"),
137
182
  temporalTaskQueue: z.string().default("opengeni-runs-ts"),
183
+ temporalTlsEnabled: EnvBoolean.default(false),
184
+ temporalApiKey: z.string().optional(),
185
+ temporalTlsServerName: z.string().optional(),
186
+ temporalTlsRootCaCertificateBase64: z.string().optional(),
187
+ temporalTlsClientCertificateBase64: z.string().optional(),
188
+ temporalTlsClientPrivateKeyBase64: z.string().optional(),
138
189
  startupDependencyRetryAttempts: z.coerce.number().int().positive().default(30),
139
190
  startupDependencyRetryInitialDelayMs: z.coerce.number().int().positive().default(1000),
140
191
  startupDependencyRetryMaxDelayMs: z.coerce.number().int().positive().default(5000),
@@ -157,11 +208,11 @@ const SettingsSchema = z.object({
157
208
  staticEntitlementsJson: z.string().default("{}"),
158
209
  staticUsageLimitsJson: z.string().default("{}"),
159
210
  delegationSecret: z.string().optional(),
160
- // Sandbox-surfacing scoped stream-token HMAC secret (master-spine §C.3 / I8).
211
+ // sandbox workspace scoped stream-token HMAC secret (sandbox contract §C.3 / stream-token availability contract).
161
212
  // When unset, the API falls back to `delegationSecret` (the same HMAC envelope
162
213
  // family, `ogs_` vs `ogd_` prefix). REQUIRED-WHEN-DESKTOP, but the absence of
163
214
  // BOTH while sandboxDesktopEnabled=true is a GRACEFUL DEGRADE (DesktopStream
164
- // transport:null + a loud boot warning), NOT a hard boot-fail (I8/OD-8).
215
+ // transport:null + a loud boot warning), NOT a hard boot-fail (stream-token availability contract).
165
216
  streamTokenSecret: z.string().optional(),
166
217
  // The desktop input plane (raw stream:control writes) is OFF in v1: even a
167
218
  // holder of stream:control gets 403 until this flips. Keeps stream:control a
@@ -169,6 +220,13 @@ const SettingsSchema = z.object({
169
220
  streamControlEnabled: EnvBoolean.default(false),
170
221
  toolspaceEnabled: EnvBoolean.default(false),
171
222
  toolspaceMaxCallsPerTurn: z.coerce.number().int().positive().default(200),
223
+ // Optional release-coherent bootstrap hint for custom rigs/connected machines
224
+ // that do not carry the stock-image ogtool binary. Exact stable versions only:
225
+ // the agent must never guess a tag or silently install `latest`.
226
+ ogtoolPackageSpec: z
227
+ .string()
228
+ .regex(/^@opengeni\/ogtool@(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u)
229
+ .optional(),
172
230
  environmentsEncryptionKey: z.string().optional(),
173
231
  integrationsEnabled: EnvBoolean.default(false),
174
232
  integrationsStateSecret: z.string().optional(),
@@ -181,10 +239,6 @@ const SettingsSchema = z.object({
181
239
  // it then acts as a hard ceiling that per-goal overrides can only lower.
182
240
  goalMaxAutoContinuations: z.coerce.number().int().positive().optional(),
183
241
  goalNoProgressLimit: z.coerce.number().int().positive().default(3),
184
- // Temporary safety valve: spawned child sessions still run and retain their
185
- // own durable events/goals, but terminal child episodes do not create parent
186
- // system updates or inference work unless explicitly enabled.
187
- childCompletionParentWakeEnabled: EnvBoolean.default(false),
188
242
  // Per-segment ceiling on agent loop turns (model calls) within a single
189
243
  // session turn. Effectively unbounded by default for the same reason as
190
244
  // above; the graceful max-turns valve (idle + goal continuation, never a
@@ -220,6 +274,10 @@ const SettingsSchema = z.object({
220
274
  // Model-catalog auto-compact limit. When present it is clamped to
221
275
  // 90% of the raw window, matching Codex core's auto_compact_token_limit().
222
276
  contextAutoCompactThresholdTokens: z.coerce.number().int().positive().optional(),
277
+ // Provider-neutral fallback for canonical model-facing tool-result text.
278
+ // The current stable Codex catalog policy is 10k tokens; the truncator adds
279
+ // Codex's 1.2x JSON serialization allowance when applying it.
280
+ modelToolOutputTruncationTokens: z.coerce.number().int().positive().default(10_000),
223
281
  authRequired: EnvBoolean.default(false),
224
282
  accessKey: z.string().optional(),
225
283
  authAllowHealth: EnvBoolean.default(true),
@@ -253,15 +311,11 @@ const SettingsSchema = z.object({
253
311
  // tool that BM25-discloses only the matching connectors. Default OFF — a codex
254
312
  // turn is byte-for-byte unchanged until enabled. OPENGENI_CODEX_TOOL_SEARCH_ENABLED
255
313
  codexToolSearchEnabled: EnvBoolean.default(false),
256
- // OPE-21 atomic, workspace-local credential allocation. Default OFF is a
314
+ // credential allocator atomic, workspace-local credential allocation. Default OFF is a
257
315
  // deliberate rolling-deploy fence: migrate + roll every worker first, then
258
316
  // enable. Turning it off restores the legacy sticky selector without a schema
259
317
  // rollback; the additive lease table/cursor columns become inert.
260
318
  codexCredentialLeasingEnabled: EnvBoolean.default(false),
261
- // Multi-account P3 (auto-rotation): an account is "near exhaustion" — ineligible to be
262
- // rotated TO — when EITHER usage window (5h/weekly) is at/over this percent. Default 90 to
263
- // match the UI danger flip (UsageBar danger at pct >= 90). OPENGENI_CODEX_ROTATION_NEAR_EXHAUSTION_PCT.
264
- codexRotationNearExhaustionPct: z.coerce.number().int().min(1).max(100).default(90),
265
319
  openaiReasoningEffort: ReasoningEffort.default("low"),
266
320
  openaiAllowedReasoningEfforts: z.string().default("low,medium,high,xhigh"),
267
321
  openaiResponsesTransport: z.enum(["http", "websocket"]).default("http"),
@@ -402,7 +456,7 @@ const SettingsSchema = z.object({
402
456
  // recordingMaxSeconds is the ffmpeg -t hard ceiling (bounds a multi-day turn).
403
457
  recordingEnabled: EnvBoolean.default(true),
404
458
  recordingDefaultCodec: z.enum(["h264-mp4", "vp9-webm"]).default("h264-mp4"),
405
- // Workbench v2 turn-end workspace capture (dossier §10.1). When on, the turn
459
+ // Workbench v2 turn-end workspace capture. When on, the turn
406
460
  // activity probes the box's changed files off the live box at turn end and
407
461
  // persists a capture revision (blobs in @opengeni/storage) so the workbench
408
462
  // paints cold/offline sessions with zero machine round-trips. Best-effort and
@@ -481,7 +535,7 @@ const SettingsSchema = z.object({
481
535
  // 404 (invisible — the surface does not exist for this deployment) and the
482
536
  // selfhosted backend is inert; boot is unaffected. EnvBoolean (NOT
483
537
  // z.coerce.boolean(), which coerces "false" -> true). Flipped per-environment via
484
- // the deploy-staging IaC secret/configmap pattern (dossier §17/§25.1).
538
+ // the deploy-staging IaC secret/configmap pattern.
485
539
  sandboxSelfhostedEnabled: EnvBoolean.default(false),
486
540
  // Gates the op-stream (streaming exec) transport to Connected Machines. The
487
541
  // runner must ALSO advertise Capabilities.op_stream; default off, and legacy
@@ -501,7 +555,7 @@ const SettingsSchema = z.object({
501
555
  selfhostedNatsUrl: z.string().optional(),
502
556
  selfhostedRelayUrl: z.string().optional(),
503
557
  // The HMAC secret the control plane signs the agent's relay PRODUCER token with
504
- // (the `ogr_` envelope threaded into EnrollmentCredentials.relayToken; M8b/dossier
558
+ // (the `ogr_` envelope threaded into EnrollmentCredentials.relayToken; M8b/design
505
559
  // §10.5). The relay verifies the producer token with the SAME secret. Optional:
506
560
  // when ABSENT the poll returns an empty relayToken (graceful degrade — the stream
507
561
  // plane is simply unavailable until configured). Falls back to streamTokenSecret /
@@ -511,7 +565,7 @@ const SettingsSchema = z.object({
511
565
  // The minisign PUBLIC key the agent pins for self-update verification (handed to
512
566
  // the agent in EnrollmentCredentials; the SECRET key lives only in CI).
513
567
  agentUpdatePublicKey: z.string().optional(),
514
- // --- NATS auth-callout tenancy boundary (bring-your-own-compute M-AUTH; dossier
568
+ // --- NATS auth-callout tenancy boundary (bring-your-own-compute M-AUTH; design
515
569
  // §10.1 NATS Accounts per workspace + §17 the isolation smoke) -------------
516
570
  // nats-server is configured with AUTH CALLOUT: an external agent connects
517
571
  // presenting its `oge_` enrollment bearer as the connect auth-token; the server
@@ -705,6 +759,19 @@ const SettingsSchema = z.object({
705
759
 
706
760
  export type Settings = z.infer<typeof SettingsSchema>;
707
761
  export type McpServerConfig = Settings["mcpServers"][number];
762
+ export type TemporalTlsConnectionConfig = {
763
+ serverNameOverride?: string;
764
+ serverRootCACertificate?: Uint8Array;
765
+ clientCertPair?: {
766
+ crt: Uint8Array;
767
+ key: Uint8Array;
768
+ };
769
+ };
770
+ export type TemporalConnectionOptions = {
771
+ address: string;
772
+ tls?: true | TemporalTlsConnectionConfig;
773
+ apiKey?: string;
774
+ };
708
775
  export type ModelPricing = {
709
776
  inputMicrosPerMillionTokens: number;
710
777
  cachedInputMicrosPerMillionTokens?: number | undefined;
@@ -754,6 +821,9 @@ const RegistryModelSchema = z.object({
754
821
  contextWindowTokens: z.number().int().positive().optional(),
755
822
  effectiveContextWindowTokens: z.number().int().positive().optional(),
756
823
  autoCompactTokenLimit: z.number().int().positive().optional(),
824
+ // Canonical model-facing function/tool-result policy. The runtime applies
825
+ // the same 1.2x serialization allowance as Codex when materializing output.
826
+ toolOutputTruncationTokens: z.number().int().positive().optional(),
757
827
  reasoningEffort: z.boolean().optional(), // model accepts a reasoning-effort control
758
828
  hostedWebSearch: z.boolean().optional(), // provider executes the hosted web_search tool for this model
759
829
  pricing: ModelPricingSchema.optional(),
@@ -812,6 +882,7 @@ export interface ConfiguredModel {
812
882
  contextWindowTokens?: number | undefined;
813
883
  effectiveContextWindowTokens?: number | undefined;
814
884
  autoCompactTokenLimit?: number | undefined;
885
+ toolOutputTruncationTokens?: number | undefined;
815
886
  reasoningEffort: boolean;
816
887
  hostedWebSearch: boolean;
817
888
  }
@@ -975,6 +1046,14 @@ export function getSettings(): Settings {
975
1046
  temporalHost: optional("OPENGENI_TEMPORAL_HOST"),
976
1047
  temporalNamespace: optional("OPENGENI_TEMPORAL_NAMESPACE"),
977
1048
  temporalTaskQueue: optional("OPENGENI_TEMPORAL_TASK_QUEUE"),
1049
+ temporalTlsEnabled: optional("OPENGENI_TEMPORAL_TLS_ENABLED"),
1050
+ temporalApiKey: optional("OPENGENI_TEMPORAL_API_KEY"),
1051
+ temporalTlsServerName: optional("OPENGENI_TEMPORAL_TLS_SERVER_NAME"),
1052
+ temporalTlsRootCaCertificateBase64: optional(
1053
+ "OPENGENI_TEMPORAL_TLS_ROOT_CA_CERTIFICATE_BASE64",
1054
+ ),
1055
+ temporalTlsClientCertificateBase64: optional("OPENGENI_TEMPORAL_TLS_CLIENT_CERTIFICATE_BASE64"),
1056
+ temporalTlsClientPrivateKeyBase64: optional("OPENGENI_TEMPORAL_TLS_CLIENT_PRIVATE_KEY_BASE64"),
978
1057
  startupDependencyRetryAttempts: optional("OPENGENI_STARTUP_DEPENDENCY_RETRY_ATTEMPTS"),
979
1058
  startupDependencyRetryInitialDelayMs: optional(
980
1059
  "OPENGENI_STARTUP_DEPENDENCY_RETRY_INITIAL_DELAY_MS",
@@ -999,6 +1078,7 @@ export function getSettings(): Settings {
999
1078
  streamControlEnabled: optional("OPENGENI_STREAM_CONTROL_ENABLED"),
1000
1079
  toolspaceEnabled: optional("OPENGENI_TOOLSPACE_ENABLED"),
1001
1080
  toolspaceMaxCallsPerTurn: optional("OPENGENI_TOOLSPACE_MAX_CALLS_PER_TURN"),
1081
+ ogtoolPackageSpec: optional("OPENGENI_OGTOOL_PACKAGE_SPEC"),
1002
1082
  environmentsEncryptionKey: optional("OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY"),
1003
1083
  integrationsEnabled: optional("OPENGENI_INTEGRATIONS_ENABLED"),
1004
1084
  integrationsStateSecret: optional("OPENGENI_INTEGRATIONS_STATE_SECRET"),
@@ -1008,13 +1088,13 @@ export function getSettings(): Settings {
1008
1088
  integrationsOauthClientsJson: optional("OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON"),
1009
1089
  goalMaxAutoContinuations: optional("OPENGENI_GOAL_MAX_AUTO_CONTINUATIONS"),
1010
1090
  goalNoProgressLimit: optional("OPENGENI_GOAL_NO_PROGRESS_LIMIT"),
1011
- childCompletionParentWakeEnabled: optional("OPENGENI_CHILD_COMPLETION_PARENT_WAKE_ENABLED"),
1012
1091
  agentMaxModelCallsPerTurn: optional("OPENGENI_AGENT_MAX_MODEL_CALLS_PER_TURN"),
1013
1092
  contextWindowTokens: optional("OPENGENI_CONTEXT_WINDOW_TOKENS"),
1014
1093
  contextEffectiveWindowTokens: optional("OPENGENI_CONTEXT_EFFECTIVE_WINDOW_TOKENS"),
1015
1094
  contextCompactionThresholdRatio: optional("OPENGENI_COMPACTION_THRESHOLD_RATIO"),
1016
1095
  contextReservedOutputTokens: optional("OPENGENI_CONTEXT_RESERVED_OUTPUT_TOKENS"),
1017
1096
  contextAutoCompactThresholdTokens: optional("OPENGENI_CONTEXT_AUTO_COMPACT_THRESHOLD_TOKENS"),
1097
+ modelToolOutputTruncationTokens: optional("OPENGENI_MODEL_TOOL_OUTPUT_TRUNCATION_TOKENS"),
1018
1098
  authRequired: optional("OPENGENI_AUTH_REQUIRED"),
1019
1099
  accessKey: optional("OPENGENI_ACCESS_KEY"),
1020
1100
  authAllowHealth: optional("OPENGENI_AUTH_ALLOW_HEALTH"),
@@ -1035,7 +1115,6 @@ export function getSettings(): Settings {
1035
1115
  codexToolSearchEnabled: optional("OPENGENI_CODEX_TOOL_SEARCH_ENABLED"),
1036
1116
  codexCredentialLeasingEnabled: optional("OPENGENI_CODEX_CREDENTIAL_LEASING_ENABLED"),
1037
1117
  codexProductSku: optional("OPENGENI_CODEX_PRODUCT_SKU"),
1038
- codexRotationNearExhaustionPct: optional("OPENGENI_CODEX_ROTATION_NEAR_EXHAUSTION_PCT"),
1039
1118
  openaiReasoningEffort: optional("OPENGENI_OPENAI_REASONING_EFFORT"),
1040
1119
  openaiAllowedReasoningEfforts: optional("OPENGENI_OPENAI_ALLOWED_REASONING_EFFORTS"),
1041
1120
  openaiResponsesTransport: optional("OPENGENI_OPENAI_RESPONSES_TRANSPORT"),
@@ -1373,6 +1452,7 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
1373
1452
  providerLabel: builtinLabel,
1374
1453
  api: "responses" as const,
1375
1454
  contextWindowTokens: settings.contextWindowTokens,
1455
+ toolOutputTruncationTokens: settings.modelToolOutputTruncationTokens,
1376
1456
  reasoningEffort: true,
1377
1457
  hostedWebSearch: settings.webSearchEnabled,
1378
1458
  }));
@@ -1394,6 +1474,9 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
1394
1474
  ...(model.autoCompactTokenLimit === undefined
1395
1475
  ? {}
1396
1476
  : { autoCompactTokenLimit: model.autoCompactTokenLimit }),
1477
+ ...(model.toolOutputTruncationTokens === undefined
1478
+ ? {}
1479
+ : { toolOutputTruncationTokens: model.toolOutputTruncationTokens }),
1397
1480
  reasoningEffort: model.reasoningEffort ?? false,
1398
1481
  hostedWebSearch: model.hostedWebSearch ?? false,
1399
1482
  });
@@ -1490,7 +1573,10 @@ export function settingsWithResolvedModelContext(
1490
1573
  settings: Settings,
1491
1574
  model: Pick<
1492
1575
  ConfiguredModel,
1493
- "contextWindowTokens" | "effectiveContextWindowTokens" | "autoCompactTokenLimit"
1576
+ | "contextWindowTokens"
1577
+ | "effectiveContextWindowTokens"
1578
+ | "autoCompactTokenLimit"
1579
+ | "toolOutputTruncationTokens"
1494
1580
  >,
1495
1581
  ): Settings {
1496
1582
  const contextWindowTokens = model.contextWindowTokens ?? settings.contextWindowTokens;
@@ -1508,6 +1594,9 @@ export function settingsWithResolvedModelContext(
1508
1594
  ...(model.autoCompactTokenLimit === undefined
1509
1595
  ? {}
1510
1596
  : { contextAutoCompactThresholdTokens: model.autoCompactTokenLimit }),
1597
+ ...(model.toolOutputTruncationTokens === undefined
1598
+ ? {}
1599
+ : { modelToolOutputTruncationTokens: model.toolOutputTruncationTokens }),
1511
1600
  };
1512
1601
  }
1513
1602
 
@@ -1578,6 +1667,77 @@ export function environmentsEncryptionKeyBytes(settings: Settings): Uint8Array |
1578
1667
  return new Uint8Array(decoded);
1579
1668
  }
1580
1669
 
1670
+ /**
1671
+ * Build one structurally compatible connection policy for both
1672
+ * `@temporalio/client` and `@temporalio/worker`. An API key or any custom TLS
1673
+ * material enables TLS automatically; the explicit flag covers server-auth TLS
1674
+ * without credentials. Secret values are never included in validation errors.
1675
+ */
1676
+ export function temporalConnectionOptions(settings: Settings): TemporalConnectionOptions {
1677
+ const apiKey = settings.temporalApiKey?.trim() || undefined;
1678
+ const serverNameOverride = settings.temporalTlsServerName?.trim() || undefined;
1679
+ const rootCa = decodeTemporalTlsMaterial(
1680
+ settings.temporalTlsRootCaCertificateBase64,
1681
+ "OPENGENI_TEMPORAL_TLS_ROOT_CA_CERTIFICATE_BASE64",
1682
+ );
1683
+ const clientCertificate = decodeTemporalTlsMaterial(
1684
+ settings.temporalTlsClientCertificateBase64,
1685
+ "OPENGENI_TEMPORAL_TLS_CLIENT_CERTIFICATE_BASE64",
1686
+ );
1687
+ const clientPrivateKey = decodeTemporalTlsMaterial(
1688
+ settings.temporalTlsClientPrivateKeyBase64,
1689
+ "OPENGENI_TEMPORAL_TLS_CLIENT_PRIVATE_KEY_BASE64",
1690
+ );
1691
+
1692
+ if (Boolean(clientCertificate) !== Boolean(clientPrivateKey)) {
1693
+ throw new Error(
1694
+ "OPENGENI_TEMPORAL_TLS_CLIENT_CERTIFICATE_BASE64 and " +
1695
+ "OPENGENI_TEMPORAL_TLS_CLIENT_PRIVATE_KEY_BASE64 must both be set or both omitted",
1696
+ );
1697
+ }
1698
+
1699
+ const tls: TemporalTlsConnectionConfig = {};
1700
+ if (serverNameOverride) {
1701
+ tls.serverNameOverride = serverNameOverride;
1702
+ }
1703
+ if (rootCa) {
1704
+ tls.serverRootCACertificate = rootCa;
1705
+ }
1706
+ if (clientCertificate && clientPrivateKey) {
1707
+ tls.clientCertPair = { crt: clientCertificate, key: clientPrivateKey };
1708
+ }
1709
+ const hasCustomTls = Object.keys(tls).length > 0;
1710
+ const tlsEnabled = settings.temporalTlsEnabled || Boolean(apiKey) || hasCustomTls;
1711
+
1712
+ return {
1713
+ address: settings.temporalHost,
1714
+ ...(tlsEnabled ? { tls: hasCustomTls ? tls : true } : {}),
1715
+ ...(apiKey ? { apiKey } : {}),
1716
+ };
1717
+ }
1718
+
1719
+ function decodeTemporalTlsMaterial(
1720
+ value: string | undefined,
1721
+ settingName: string,
1722
+ ): Uint8Array | undefined {
1723
+ // RFC 2045 base64 commonly arrives wrapped at 76 columns. Kubernetes
1724
+ // stringData and external secret stores preserve those line breaks, so
1725
+ // normalize whitespace before applying the strict alphabet/canonical check.
1726
+ const encoded = value?.replace(/\s/g, "");
1727
+ if (!encoded) {
1728
+ return undefined;
1729
+ }
1730
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded) || encoded.length % 4 === 1) {
1731
+ throw new Error(`${settingName} must contain valid base64`);
1732
+ }
1733
+ const decoded = Buffer.from(encoded, "base64");
1734
+ const canonical = decoded.toString("base64").replace(/=+$/, "");
1735
+ if (decoded.length === 0 || canonical !== encoded.replace(/=+$/, "")) {
1736
+ throw new Error(`${settingName} must contain valid base64`);
1737
+ }
1738
+ return new Uint8Array(decoded);
1739
+ }
1740
+
1581
1741
  /**
1582
1742
  * The connection `search_path` for OpenGeni's db handles + the managed-auth pool
1583
1743
  * (Step I, §7.8 runtime half). Returns `undefined` when `dbSchema` is unset
@@ -1585,7 +1745,7 @@ export function environmentsEncryptionKeyBytes(settings: Settings): Uint8Array |
1585
1745
  * default (`public`) applies — byte-for-byte today's behavior. When `dbSchema`
1586
1746
  * is set (embedded), returns `"<schema>,opengeni_private,public"` — `public`
1587
1747
  * stays LAST so `gen_random_uuid()` (pgcrypto) and the `vector` type still
1588
- * resolve (the SPIKE-1 live footgun). `opengeni_private` is on the path so the
1748
+ * resolve (the schema-isolation contract live footgun). `opengeni_private` is on the path so the
1589
1749
  * RLS GUC-reader helpers resolve when referenced unqualified.
1590
1750
  */
1591
1751
  export function dbSearchPath(settings: Pick<Settings, "dbSchema">): string | undefined {
@@ -1688,6 +1848,9 @@ export function stableSandboxEnvironmentForRun(
1688
1848
  }
1689
1849
  if (settings.toolspaceEnabled) {
1690
1850
  environment.OPENGENI_TOOLSPACE_TOKEN_FILE ??= `${environment.HOME ?? descriptor.workspaceRoot}/.opengeni/toolspace-token`;
1851
+ if (settings.ogtoolPackageSpec) {
1852
+ environment.OPENGENI_OGTOOL_PACKAGE_SPEC ??= settings.ogtoolPackageSpec;
1853
+ }
1691
1854
  if (options.workspaceId) {
1692
1855
  environment.OPENGENI_TOOLSPACE_URL ??= firstPartyMcpWorkspaceUrl(
1693
1856
  settings,
@@ -2201,6 +2364,7 @@ function firstPartyDocumentsMcpServerUrl(mcpUrl: string): string {
2201
2364
  }
2202
2365
 
2203
2366
  function validateSettings(settings: Settings): void {
2367
+ temporalConnectionOptions(settings);
2204
2368
  if (settings.toolspaceEnabled && !settings.delegationSecret) {
2205
2369
  throw new Error("OPENGENI_DELEGATION_SECRET is required when OPENGENI_TOOLSPACE_ENABLED=true");
2206
2370
  }
@@ -2512,7 +2676,7 @@ function validateSettings(settings: Settings): void {
2512
2676
  );
2513
2677
  }
2514
2678
  }
2515
- // --- stream-token secret: required-when-desktop, but GRACEFULLY DEGRADE (I8) ---
2679
+ // --- stream-token secret: required-when-desktop, but GRACEFULLY DEGRADE (stream-token availability contract) ---
2516
2680
  // The desktop pixel plane needs an HMAC secret to mint scoped stream tokens.
2517
2681
  // It is REQUIRED when desktop is enabled — but per OD-8 a missing secret is NOT
2518
2682
  // a hard boot-fail: we emit a LOUD warning and the deployment ships with
@@ -2559,7 +2723,7 @@ function validateSettings(settings: Settings): void {
2559
2723
  }
2560
2724
 
2561
2725
  /**
2562
- * Resolve the secret used to sign/verify scoped stream tokens (master-spine
2726
+ * Resolve the secret used to sign/verify scoped stream tokens (sandbox contract
2563
2727
  * §C.3). Falls back to `delegationSecret` (the same HMAC envelope family —
2564
2728
  * `ogs_` vs `ogd_` prefix) so a deployment that already carries a delegation
2565
2729
  * secret does not need a second one. Returns undefined when neither is set,
@@ -2576,7 +2740,7 @@ export function resolveStreamTokenSecret(settings: Settings): string | undefined
2576
2740
 
2577
2741
  /**
2578
2742
  * True iff the desktop pixel plane must GRACEFULLY DEGRADE because desktop is
2579
- * enabled but no stream-token secret is resolvable (I8/OD-8). When true,
2743
+ * enabled but no stream-token secret is resolvable (stream-token availability contract). When true,
2580
2744
  * negotiateCapabilities forces DesktopStream.transport:null.
2581
2745
  */
2582
2746
  export function streamTokenDegraded(settings: Settings): boolean {
@@ -2585,7 +2749,7 @@ export function streamTokenDegraded(settings: Settings): boolean {
2585
2749
 
2586
2750
  /**
2587
2751
  * Resolve the secret the control plane signs the enrollment bearer credential
2588
- * with (the `oge_` envelope the agent presents back — M5/dossier §10.2). Falls
2752
+ * with (the `oge_` envelope the agent presents back — M5). Falls
2589
2753
  * back to `delegationSecret` (the same HMAC envelope family) so a deployment that
2590
2754
  * already carries a delegation secret needs no second one. Returns undefined when
2591
2755
  * neither is set; when selfhosted is enabled but this is undefined, the poll route
@@ -2603,7 +2767,7 @@ export function resolveEnrollmentSigningSecret(settings: Settings): string | und
2603
2767
 
2604
2768
  /**
2605
2769
  * Resolve the HMAC secret the control plane signs the agent's relay PRODUCER token
2606
- * with (the `ogr_` envelope; M8b/dossier §10.5). The RELAY verifies the producer
2770
+ * with (the `ogr_` envelope; M8b). The RELAY verifies the producer
2607
2771
  * token with the SAME secret (injected into the relay via env). Prefers an explicit
2608
2772
  * `selfhostedRelayTokenSecret`, then the `streamTokenSecret` (the relay already
2609
2773
  * needs that one to verify the viewer's `ogs_` token, so a single secret can back