@omercnet/paseo-omp 0.4.0 → 1.0.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.
Files changed (40) hide show
  1. package/README.md +8 -8
  2. package/client/external-url.ts +2 -11
  3. package/client/omp-config-styles.ts +284 -0
  4. package/client/omp-config-surface.tsx +16 -286
  5. package/client/omp-routing-editor.tsx +1 -3
  6. package/client/provider-launch-settings.tsx +118 -0
  7. package/index.client.tsx +21 -2
  8. package/index.server.ts +10 -0
  9. package/package.json +14 -9
  10. package/paseo-plugin.json +2 -1
  11. package/scripts/prepare-dependencies.mjs +44 -11
  12. package/server/omp-models.ts +2 -6
  13. package/server/package-version.ts +1 -1
  14. package/server/protocol-violation-diagnostics.ts +1 -1
  15. package/server/provider/catalog.ts +2 -1
  16. package/server/provider/config-normalization.ts +19 -3
  17. package/server/provider/connection.ts +30 -0
  18. package/server/provider/host-tools.ts +2 -2
  19. package/server/provider/mcp-transport.ts +9 -2
  20. package/server/provider/omp-rpc-environment.ts +368 -0
  21. package/server/provider/omp-rpc-process.ts +119 -0
  22. package/server/provider/omp-rpc-protocol.ts +1949 -0
  23. package/server/provider/omp-rpc-transport.ts +1373 -0
  24. package/server/provider/omp-rpc-values.ts +31 -0
  25. package/server/provider/omp-rpc.ts +267 -3311
  26. package/server/provider/profile-providers.ts +2 -1
  27. package/server/provider/prompt-payload.ts +253 -0
  28. package/server/provider/registration.ts +10 -2
  29. package/server/provider/session-compaction.ts +187 -0
  30. package/server/provider/session-descriptors.ts +52 -18
  31. package/server/provider/session-events.ts +42 -0
  32. package/server/provider/session-permissions.ts +987 -0
  33. package/server/provider/session-terminal.ts +307 -0
  34. package/server/provider/session-usage.ts +234 -0
  35. package/server/provider/session.ts +145 -1853
  36. package/server/provider/settings.ts +8 -5
  37. package/server/provider/subsessions.ts +30 -9
  38. package/server/provider/timeline-projector.ts +140 -29
  39. package/shared/external-url.ts +0 -9
  40. package/shared/provider-launch-settings.ts +18 -0
@@ -1,21 +1,54 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
 
6
6
  const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)));
7
7
 
8
- export function prepareDependencies(root = projectRoot, execute = execFileSync) {
9
- if (!existsSync(join(root, "package-lock.json"))) return false;
8
+ function loadRootCatalog(root) {
9
+ for (let directory = root; ; directory = dirname(directory)) {
10
+ const manifestPath = join(directory, "package.json");
11
+ if (existsSync(manifestPath)) {
12
+ const catalog = JSON.parse(readFileSync(manifestPath, "utf8")).catalog;
13
+ if (catalog) return catalog;
14
+ }
15
+ if (directory === dirname(directory)) return null;
16
+ }
17
+ }
18
+
19
+ function stageManifest(root, catalog, install) {
20
+ const manifestPath = join(root, "package.json");
21
+ const source = readFileSync(manifestPath, "utf8");
22
+ const manifest = JSON.parse(source);
23
+
24
+ for (const [name, spec] of Object.entries(manifest.dependencies ?? {})) {
25
+ if (spec !== "catalog:") continue;
26
+ if (typeof catalog[name] !== "string") throw new Error(`Missing catalog entry for ${name}`);
27
+ manifest.dependencies[name] = catalog[name];
28
+ }
29
+ delete manifest.devDependencies;
10
30
 
11
- execute(
12
- process.platform === "win32" ? "npm.cmd" : "npm",
13
- ["ci", "--omit=dev", "--ignore-scripts"],
14
- {
15
- cwd: root,
16
- stdio: "inherit",
17
- },
18
- );
31
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
32
+ try {
33
+ install();
34
+ } finally {
35
+ writeFileSync(manifestPath, source);
36
+ }
37
+ }
38
+
39
+ export function prepareDependencies(root = projectRoot, execute = execFileSync) {
40
+ const catalog = loadRootCatalog(root);
41
+ if (!catalog) return false;
42
+ stageManifest(root, catalog, () => {
43
+ execute(
44
+ process.platform === "win32" ? "npm.cmd" : "npm",
45
+ ["install", "--omit=dev", "--ignore-scripts", "--no-package-lock", "--workspaces=false"],
46
+ {
47
+ cwd: root,
48
+ stdio: "inherit",
49
+ },
50
+ );
51
+ });
19
52
  return true;
20
53
  }
21
54
 
@@ -3,12 +3,8 @@ import { isAbsolute } from "node:path";
3
3
  import type { RpcInput } from "@getpaseo/plugin";
4
4
  import type { listOmpModels, OmpModelCandidate, OmpModelListResult } from "../shared/omp-models";
5
5
  import { currentOmpEnvironment } from "./paths";
6
- import {
7
- type OmpModel,
8
- OmpRpcRuntime,
9
- type OmpRuntime,
10
- type OmpRuntimeSession,
11
- } from "./provider/omp-rpc";
6
+ import { OmpRpcRuntime, type OmpRuntime, type OmpRuntimeSession } from "./provider/omp-rpc";
7
+ import type { OmpModel } from "./provider/omp-rpc-protocol";
12
8
  import { OmpCleanupFailure, OmpPublicDataSerializer, OmpPublicError } from "./provider/security";
13
9
 
14
10
  function mapOmpModel(model: OmpModel, serializer: OmpPublicDataSerializer): OmpModelCandidate {
@@ -1,2 +1,2 @@
1
1
  /** Release Please keeps this bundle-safe constant aligned with package.json. */
2
- export const PASEO_OMP_PACKAGE_VERSION = "0.4.0"; // x-release-please-version
2
+ export const PASEO_OMP_PACKAGE_VERSION = "1.0.0"; // x-release-please-version
@@ -4,7 +4,7 @@ import {
4
4
  type OmpProtocolViolationCategory,
5
5
  type OmpProtocolViolationDiagnostic,
6
6
  type OmpProtocolViolationReason,
7
- } from "./provider/omp-rpc";
7
+ } from "./provider/omp-rpc-protocol";
8
8
 
9
9
  export interface OmpProtocolViolationSummary {
10
10
  category: OmpProtocolViolationCategory;
@@ -6,7 +6,8 @@ import type {
6
6
  ProviderThinkingOption,
7
7
  } from "@getpaseo/plugin/server/provider";
8
8
  import type { NormalizedOmpStartOptions } from "./config-normalization";
9
- import type { OmpModel, OmpRuntime, OmpRuntimeSession } from "./omp-rpc";
9
+ import type { OmpRuntime, OmpRuntimeSession } from "./omp-rpc";
10
+ import type { OmpModel } from "./omp-rpc-protocol";
10
11
  import {
11
12
  configuredOutputRedactionValues,
12
13
  OmpCleanupFailure,
@@ -1,5 +1,5 @@
1
1
  import type { ProviderSessionConfig } from "@getpaseo/plugin/server/provider";
2
- import type { OmpStartOptions } from "./omp-rpc";
2
+ import type { OmpStartOptions } from "./omp-rpc-environment";
3
3
 
4
4
  type ProviderCatalogOptionsCompat = {
5
5
  scope: "global" | "workspace";
@@ -9,6 +9,7 @@ type ProviderCatalogOptionsCompat = {
9
9
  settings?: Readonly<Record<string, unknown>>;
10
10
  };
11
11
 
12
+ import { MAX_INHERITED_ENVIRONMENT_NAMES } from "../../shared/provider-launch-settings";
12
13
  import { parseOmpProviderOptions } from "./provider-options";
13
14
  import { OmpPublicError } from "./security";
14
15
  import { OmpModeSchema } from "./settings";
@@ -59,11 +60,24 @@ function allowedOmpTools(deniedTools: readonly string[] | undefined): string[] |
59
60
 
60
61
  export type NormalizedOmpStartOptions = Omit<OmpStartOptions, "environment" | "signal">;
61
62
  export type OmpRecoveryOptions = Omit<OmpStartOptions, "resumeSessionId" | "signal">;
63
+ export function mergeOmpInheritedEnvironmentNames(
64
+ hostNames: readonly string[],
65
+ profileNames: readonly string[] | undefined,
66
+ ): string[] | undefined {
67
+ const names = [...new Set([...hostNames, ...(profileNames ?? [])])];
68
+ if (names.length > MAX_INHERITED_ENVIRONMENT_NAMES) {
69
+ throw new OmpPublicError("OMP inherited environment has too many entries");
70
+ }
71
+ return names.length > 0 ? names : undefined;
72
+ }
73
+
62
74
  export function normalizeOmpCatalogOptions(
63
75
  options: ProviderCatalogOptionsCompat,
64
76
  cwd: string,
77
+ hostInheritEnv: readonly string[] = [],
65
78
  ): Omit<OmpStartOptions, "environment" | "signal"> {
66
79
  const providerOptions = parseOmpProviderOptions(options.providerOptions);
80
+ const inheritEnv = mergeOmpInheritedEnvironmentNames(hostInheritEnv, providerOptions.inheritEnv);
67
81
  const params = providerOptions.params ?? {};
68
82
  return {
69
83
  cwd,
@@ -71,7 +85,7 @@ export function normalizeOmpCatalogOptions(
71
85
  noSession: true,
72
86
  ...(providerOptions.command ? { command: providerOptions.command } : {}),
73
87
  ...(providerOptions.env ? { env: providerOptions.env } : {}),
74
- ...(providerOptions.inheritEnv ? { inheritEnv: providerOptions.inheritEnv } : {}),
88
+ ...(inheritEnv ? { inheritEnv } : {}),
75
89
  outputRedaction: providerOptions.outputRedaction,
76
90
  ...(params.sessionDir ? { sessionDir: params.sessionDir } : {}),
77
91
  ...(params.rpcTimeoutMs
@@ -103,6 +117,7 @@ export function withCommittedOmpSelection(
103
117
  export function normalizeOmpSessionConfig(
104
118
  config: ProviderSessionConfig,
105
119
  permissionSupported = false,
120
+ hostInheritEnv: readonly string[] = [],
106
121
  ): NormalizedOmpStartOptions {
107
122
  if (Object.keys(config.settings).length > 0) {
108
123
  throw new OmpPublicError("OMP does not expose live provider settings");
@@ -122,11 +137,12 @@ export function normalizeOmpSessionConfig(
122
137
  const tools = allowedOmpTools(deniedTools);
123
138
  const params = options.params ?? {};
124
139
  const env = { ...options.env, ...config.env };
140
+ const inheritEnv = mergeOmpInheritedEnvironmentNames(hostInheritEnv, options.inheritEnv);
125
141
  return {
126
142
  cwd: config.cwd,
127
143
  ...(options.command ? { command: options.command } : {}),
128
144
  ...(Object.keys(env).length > 0 ? { env } : {}),
129
- ...(options.inheritEnv ? { inheritEnv: options.inheritEnv } : {}),
145
+ ...(inheritEnv ? { inheritEnv } : {}),
130
146
  outputRedaction: options.outputRedaction,
131
147
  mode: parsedMode.data,
132
148
  thinkingOption: config.thinkingOption,
@@ -17,6 +17,13 @@ import { discoverOmpCatalog } from "./catalog";
17
17
  import { normalizeOmpCatalogOptions } from "./config-normalization";
18
18
  import type { OmpMcpConnector } from "./host-tools";
19
19
  import type { OmpRuntime } from "./omp-rpc";
20
+ import {
21
+ OMP_RPC_BOUND_DIMENSIONS,
22
+ OMP_RPC_DIAGNOSTIC_COMMANDS,
23
+ type OmpRpcBoundDimension,
24
+ type OmpRpcDiagnosticCommand,
25
+ } from "./omp-rpc-protocol";
26
+ import { OmpRpcResponseLimitError } from "./omp-rpc-transport";
20
27
  import {
21
28
  boundedJsonBytes,
22
29
  isOmpCleanupFailure,
@@ -320,6 +327,10 @@ export interface OmpConnectionDiagnostic {
320
327
  code?: (typeof SYSTEM_ERROR_CODES)[number] | (typeof DATABASE_ERROR_CODES)[number];
321
328
  exitCode?: number;
322
329
  signal?: (typeof EXIT_SIGNALS)[number];
330
+ command?: OmpRpcDiagnosticCommand;
331
+ bound?: OmpRpcBoundDimension;
332
+ actual?: number;
333
+ limit?: number;
323
334
  }
324
335
 
325
336
  // Match complete, locally authored messages only. Never log an arbitrary message, error name,
@@ -428,6 +439,22 @@ function classifyFailure(
428
439
  : "NonError",
429
440
  classification: "unexpected",
430
441
  };
442
+ if (error instanceof OmpRpcResponseLimitError) {
443
+ const command = OMP_RPC_DIAGNOSTIC_COMMANDS.find((value) => value === error.command);
444
+ const bound = OMP_RPC_BOUND_DIMENSIONS.find((value) => value === error.bound);
445
+ const actual =
446
+ Number.isSafeInteger(error.actual) && error.actual >= 0 ? error.actual : undefined;
447
+ const limit = Number.isSafeInteger(error.limit) && error.limit >= 0 ? error.limit : undefined;
448
+ return {
449
+ ...result,
450
+ classification: "rpc-response-limit",
451
+ stage: "rpc",
452
+ ...(command ? { command } : {}),
453
+ ...(bound ? { bound } : {}),
454
+ ...(actual !== undefined ? { actual } : {}),
455
+ ...(limit !== undefined ? { limit } : {}),
456
+ };
457
+ }
431
458
  const message = error instanceof Error ? error.message : undefined;
432
459
  if (typeof message === "string") {
433
460
  const known = KNOWN_FAILURES.get(message);
@@ -688,6 +715,7 @@ export function createOmpConnection(
688
715
  reportDiagnostic: (diagnostic: OmpConnectionDiagnostic) => void = (diagnostic) =>
689
716
  console.error("OMP provider failure", diagnostic),
690
717
  reportOperationalFailure: OmpOperationalFailureReporter = () => {},
718
+ resolveHostInheritEnv?: () => Promise<readonly string[]>,
691
719
  ): ProviderConnection {
692
720
  const errorDetails = (error: unknown, fallback: string): { message: string } => {
693
721
  if (isOmpPublicError(error)) return { message: error.message };
@@ -795,6 +823,7 @@ export function createOmpConnection(
795
823
  settings: configuredInput.settings,
796
824
  },
797
825
  input.cwd ?? homedir(),
826
+ (await resolveHostInheritEnv?.()) ?? [],
798
827
  );
799
828
  const catalog = await discoverOmpCatalog(
800
829
  runtime,
@@ -941,6 +970,7 @@ export function createOmpConnection(
941
970
  mcpConnector,
942
971
  mcpInitializationTimeoutMs,
943
972
  reportOperationalFailure,
973
+ (await resolveHostInheritEnv?.()) ?? [],
944
974
  );
945
975
  const discoveredNativeSessionId = session.persistenceSessionId;
946
976
  if (discoveredNativeSessionId) nativeSessionId = discoveredNativeSessionId;
@@ -14,14 +14,14 @@ import {
14
14
  type ConnectedMcpToolPage,
15
15
  connectMcpServer,
16
16
  } from "./mcp-transport";
17
+ import type { OmpRuntimeSession } from "./omp-rpc";
17
18
  import {
18
19
  OMP_HOST_TOOL_FRAME_LIMIT_ERROR,
19
20
  type OmpHostToolCall,
20
21
  type OmpHostToolDefinition,
21
22
  type OmpHostToolResult,
22
- type OmpRuntimeSession,
23
23
  parseOmpHostToolAgentResult,
24
- } from "./omp-rpc";
24
+ } from "./omp-rpc-protocol";
25
25
  import {
26
26
  boundedJsonBytes,
27
27
  OmpCleanupFailure,
@@ -7,7 +7,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
7
7
  import { deserializeMessage, serializeMessage } from "@modelcontextprotocol/sdk/shared/stdio.js";
8
8
  import type { FetchLike, Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
9
9
  import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
10
- import { terminateSpawnedProcessTree } from "./omp-rpc";
10
+ import { terminateSpawnedProcessTree } from "./omp-rpc-process";
11
11
  import { OmpCleanupFailure } from "./security";
12
12
 
13
13
  const MAX_MCP_TRANSPORT_FRAME_BYTES = 1024 * 1024;
@@ -225,7 +225,7 @@ export class SupervisedStdioClientTransport implements Transport {
225
225
  return;
226
226
  }
227
227
  const treeCleanup = this.startTreeCleanup();
228
- if (!this.exited) {
228
+ if (this.platform !== "win32" && !this.exited) {
229
229
  try {
230
230
  child.stdin.end();
231
231
  } catch {
@@ -233,6 +233,13 @@ export class SupervisedStdioClientTransport implements Transport {
233
233
  }
234
234
  }
235
235
  const terminated = await treeCleanup;
236
+ if (this.platform === "win32" && terminated && !this.exited) {
237
+ try {
238
+ child.stdin.end();
239
+ } catch {
240
+ // Process-tree cleanup remains authoritative when the input channel is already closed.
241
+ }
242
+ }
236
243
  const exited =
237
244
  this.spawnFailedWithoutProcess ||
238
245
  this.exited ||
@@ -0,0 +1,368 @@
1
+ import { isAbsolute } from "node:path";
2
+ import {
3
+ MAX_MODEL_SELECTOR_BYTES,
4
+ MAX_PATH_LENGTH,
5
+ MAX_SYSTEM_PROMPT_LENGTH,
6
+ OmpThinkingLevelSchema,
7
+ validateBoundedText,
8
+ } from "./omp-rpc-values";
9
+ import { OmpPublicError, utf8Bytes } from "./security";
10
+ import { validateNativeSessionId } from "./session-descriptors";
11
+ import type { OmpOutputRedaction } from "./settings";
12
+
13
+ const MAX_ENV_ENTRIES = 256;
14
+ const MAX_ENV_VALUE_LENGTH = 64 * 1024;
15
+ const MAX_ENV_TOTAL_LENGTH = 1024 * 1024;
16
+
17
+ export interface OmpStartOptions {
18
+ cwd: string;
19
+ env?: Readonly<Record<string, string>>;
20
+ outputRedaction?: OmpOutputRedaction;
21
+ inheritEnv?: readonly string[];
22
+ /** Server-owned environment source; tests provide isolated roots instead of ambient process.env. */
23
+ environment?: NodeJS.ProcessEnv;
24
+ command?: readonly string[];
25
+ model?: string;
26
+ mode?: "full" | "write" | "ask";
27
+ thinkingOption?: string;
28
+ systemPrompt?: string;
29
+ roleModels?: Readonly<{ smol?: string; slow?: string; plan?: string }>;
30
+ tools?: readonly string[];
31
+ sessionDir?: string;
32
+ readyTimeoutMs?: number;
33
+ requestTimeoutMs?: number;
34
+ /** Resume this exact native OMP session; never use this to start a new conversation. */
35
+ resumeSessionId?: string;
36
+ noSession?: boolean;
37
+ signal?: AbortSignal;
38
+ }
39
+
40
+ export interface OmpSpawnRequest {
41
+ command: string;
42
+ args: string[];
43
+ cwd: string;
44
+ env: NodeJS.ProcessEnv;
45
+ detached: boolean;
46
+ inheritedRedactionValues: string[];
47
+ }
48
+
49
+ // The daemon contributes only process/runtime discovery variables plus provider authentication
50
+ // families. Session-scoped values are explicit host input and are overlaid after rejecting loader
51
+ // and executable-resolution controls; this keeps provider credentials available without copying
52
+ const INHERITED_RUNTIME_ENV: Readonly<Record<string, true>> = {
53
+ ALL_PROXY: true,
54
+ APPDATA: true,
55
+ COLORTERM: true,
56
+ HOME: true,
57
+ HTTPS_PROXY: true,
58
+ HTTP_PROXY: true,
59
+ LANG: true,
60
+ LC_ALL: true,
61
+ LC_CTYPE: true,
62
+ LOCALAPPDATA: true,
63
+ LOGNAME: true,
64
+ NO_PROXY: true,
65
+ OMP_PROFILE: true,
66
+ PATH: true,
67
+ PATHEXT: true,
68
+ PI_CODING_AGENT_DIR: true,
69
+ PI_CONFIG_DIR: true,
70
+ PI_PROFILE: true,
71
+ SHELL: true,
72
+ SSH_AUTH_SOCK: true,
73
+ SSL_CERT_DIR: true,
74
+ SSL_CERT_FILE: true,
75
+ SYSTEMROOT: true,
76
+ TEMP: true,
77
+ TMP: true,
78
+ TMPDIR: true,
79
+ TZ: true,
80
+ USER: true,
81
+ USERPROFILE: true,
82
+ XDG_CACHE_HOME: true,
83
+ XDG_CONFIG_HOME: true,
84
+ XDG_DATA_HOME: true,
85
+ XDG_STATE_HOME: true,
86
+ XDG_RUNTIME_DIR: true,
87
+ };
88
+ const INHERITED_PROVIDER_AUTH_ENV: Readonly<Record<string, true>> = {
89
+ AI_GATEWAY_API_KEY: true,
90
+ AIAND_API_KEY: true,
91
+ ALIBABA_CODING_PLAN_API_KEY: true,
92
+ ALIBABA_TOKEN_PLAN_API_KEY: true,
93
+ ANTHROPIC_API_KEY: true,
94
+ ANTHROPIC_FOUNDRY_API_KEY: true,
95
+ ANTHROPIC_OAUTH_TOKEN: true,
96
+ AWS_ACCESS_KEY_ID: true,
97
+ AWS_DEFAULT_REGION: true,
98
+ AWS_PROFILE: true,
99
+ AWS_REGION: true,
100
+ AWS_SECRET_ACCESS_KEY: true,
101
+ AWS_SESSION_TOKEN: true,
102
+ AZURE_CLIENT_ID: true,
103
+ AZURE_CLIENT_SECRET: true,
104
+ AZURE_OPENAI_API_KEY: true,
105
+ AZURE_OPENAI_ENDPOINT: true,
106
+ AZURE_TENANT_ID: true,
107
+ BAILIAN_TOKEN_PLAN_API_KEY: true,
108
+ BASETEN_API_KEY: true,
109
+ CEREBRAS_API_KEY: true,
110
+ CHARM_HYPER_API_KEY: true,
111
+ CLINE_API_KEY: true,
112
+ CLOUDFLARE_AI_GATEWAY_API_KEY: true,
113
+ COHERE_API_KEY: true,
114
+ COMMAND_CODE_API_KEY: true,
115
+ COREWEAVE_API_KEY: true,
116
+ CURSOR_ACCESS_TOKEN: true,
117
+ DEEPINFRA_API_KEY: true,
118
+ DEEPSEEK_API_KEY: true,
119
+ DEVIN_API_KEY: true,
120
+ FIREPASS_API_KEY: true,
121
+ FIREWORKS_API_KEY: true,
122
+ FUGU_API_KEY: true,
123
+ GEMINI_API_KEY: true,
124
+ GMI_API_KEY: true,
125
+ GOOGLE_API_KEY: true,
126
+ GOOGLE_APPLICATION_CREDENTIALS: true,
127
+ GROQ_API_KEY: true,
128
+ HF_TOKEN: true,
129
+ HUGGINGFACE_HUB_TOKEN: true,
130
+ LLAMA_CPP_API_KEY: true,
131
+ LM_STUDIO_API_KEY: true,
132
+ META_API_KEY: true,
133
+ MINIMAX_API_KEY: true,
134
+ MINIMAX_CODE_API_KEY: true,
135
+ MINIMAX_CODE_CN_API_KEY: true,
136
+ MISTRAL_API_KEY: true,
137
+ MODEL_API_KEY: true,
138
+ MOONSHOT_API_KEY: true,
139
+ NANO_GPT_API_KEY: true,
140
+ NVIDIA_API_KEY: true,
141
+ NOVITA_API_KEY: true,
142
+ OLLAMA_API_KEY: true,
143
+ OLLAMA_CLOUD_API_KEY: true,
144
+ OLLAMA_HOST: true,
145
+ OMP_AUTH_BROKER_TOKEN: true,
146
+ OMP_AUTH_BROKER_URL: true,
147
+ OPENCODE_API_KEY: true,
148
+ OPENAI_API_KEY: true,
149
+ OPENAI_CODEX_OAUTH_TOKEN: true,
150
+ OPENROUTER_API_KEY: true,
151
+ PLEXUS_API_KEY: true,
152
+ QIANFAN_API_KEY: true,
153
+ QWEN_OAUTH_TOKEN: true,
154
+ QWEN_PORTAL_API_KEY: true,
155
+ SAKANA_API_KEY: true,
156
+ SILICONFLOW_API_KEY: true,
157
+ SILICONFLOW_CN_API_KEY: true,
158
+ SYNTHETIC_API_KEY: true,
159
+ TOGETHER_API_KEY: true,
160
+ UMANS_AI_CODING_PLAN_API_KEY: true,
161
+ VENICE_API_KEY: true,
162
+ VLLM_API_KEY: true,
163
+ WAFER_SERVERLESS_API_KEY: true,
164
+ WANDB_API_KEY: true,
165
+ XAI_API_KEY: true,
166
+ XAI_OAUTH_TOKEN: true,
167
+ XIAOMI_API_KEY: true,
168
+ XIAOMI_TOKEN_PLAN_AMS_API_KEY: true,
169
+ XIAOMI_TOKEN_PLAN_CN_API_KEY: true,
170
+ XIAOMI_TOKEN_PLAN_SGP_API_KEY: true,
171
+ YOLO_AUTO_API_KEY: true,
172
+ ZAI_API_KEY: true,
173
+ ZENMUX_API_KEY: true,
174
+ ZHIPU_API_KEY: true,
175
+ };
176
+ const BLOCKED_SESSION_ENV =
177
+ /^(?:BASH_ENV|BUN_INSTALL.*|BUN_OPTIONS|CLASSPATH|CLAUDE_BASH_NO_CI|CLAUDE_BASH_NO_LOGIN|CLAUDE_CODE_SHELL_PREFIX|DYLD_.*|EDITOR|ELECTRON_RUN_AS_NODE|ENV|GEM_HOME|GEM_PATH|GIT_CONFIG.*|GIT_SSH_COMMAND|HOME|JAVA_TOOL_OPTIONS|LD_.*|NODE_OPTIONS|NODE_PATH|NPM_CONFIG_.*|OMP_AUTORESEARCH_DB_DIR|OMP_COMMAND|OMP_GITHUB_CACHE_DB|OMP_PROFILE|OMP_WORKTREE_DIR|PATH|PATHEXT|PERL5LIB|PERL5OPT|PI_BASH_NO_CI|PI_BASH_NO_LOGIN|PI_CODING_AGENT_DIR|PI_CODING_AGENT_SESSION_DIR|PI_CONFIG_DIR|PI_CONFIG_FILES|PI_GIT_COMMON_DIR|PI_PACKAGE_DIR|PI_PROFILE|PI_PROJECT_DIR|PI_SESSION_ID|PI_SHELL_PREFIX|PI_SUBPROCESS_CMD|PI_WORKTREE_DIR|PWD|PYTHONHOME|PYTHONINSPECT|PYTHONPATH|PYTHONSTARTUP|RUBYLIB|RUBYOPT|SHELL|SYSTEMROOT|USERPROFILE|VISUAL|XDG_CACHE_HOME|XDG_CONFIG_HOME|XDG_DATA_HOME|XDG_RUNTIME_DIR|XDG_STATE_HOME|_JAVA_OPTIONS)$/u;
178
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/u;
179
+
180
+ function inheritedEnvironmentNames(inheritEnv: readonly string[] | undefined): ReadonlySet<string> {
181
+ if (inheritEnv === undefined) return new Set();
182
+ if (!Array.isArray(inheritEnv)) {
183
+ throw new Error("OMP inherited environment names are invalid");
184
+ }
185
+ if (inheritEnv.length > MAX_ENV_ENTRIES) {
186
+ throw new Error("OMP inherited environment has too many entries");
187
+ }
188
+ const names = new Set<string>();
189
+ for (const name of inheritEnv) {
190
+ if (typeof name !== "string" || !ENV_NAME.test(name)) {
191
+ throw new Error("OMP inherited environment contains an invalid name");
192
+ }
193
+ if (BLOCKED_SESSION_ENV.test(name.toUpperCase())) {
194
+ throw new Error("OMP inherited environment contains a forbidden variable");
195
+ }
196
+ names.add(name);
197
+ }
198
+ return names;
199
+ }
200
+
201
+ function buildOmpEnvironment(
202
+ sessionEnv: Readonly<Record<string, string>> | undefined,
203
+ inheritEnv: readonly string[] | undefined,
204
+ sourceEnv: NodeJS.ProcessEnv,
205
+ ): { env: NodeJS.ProcessEnv; inheritedRedactionValues: string[] } {
206
+ if (
207
+ sessionEnv !== undefined &&
208
+ (sessionEnv === null || typeof sessionEnv !== "object" || Array.isArray(sessionEnv))
209
+ ) {
210
+ throw new Error("OMP session environment is invalid");
211
+ }
212
+ const explicitlyInherited = inheritedEnvironmentNames(inheritEnv);
213
+ const explicitlyInheritedNormalized = new Set(
214
+ [...explicitlyInherited].map((name) => name.toUpperCase()),
215
+ );
216
+ const explicitNames = new Set(Object.keys(sessionEnv ?? {}).map((name) => name.toUpperCase()));
217
+ const env: NodeJS.ProcessEnv = {};
218
+ const inheritedRedactionValues: string[] = [];
219
+ let totalBytes = 0;
220
+ for (const [name, value] of Object.entries(sourceEnv)) {
221
+ if (value === undefined || name.toUpperCase() === "OMP_COMMAND") continue;
222
+ const normalizedName = name.toUpperCase();
223
+ const isRuntime =
224
+ process.platform === "win32"
225
+ ? normalizedName in INHERITED_RUNTIME_ENV
226
+ : name in INHERITED_RUNTIME_ENV;
227
+ const isProviderAuth =
228
+ process.platform === "win32"
229
+ ? normalizedName in INHERITED_PROVIDER_AUTH_ENV
230
+ : name in INHERITED_PROVIDER_AUTH_ENV;
231
+ const isExplicitlyInherited =
232
+ process.platform === "win32"
233
+ ? explicitlyInheritedNormalized.has(normalizedName)
234
+ : explicitlyInherited.has(name);
235
+ if (!isRuntime && !isProviderAuth && !isExplicitlyInherited) continue;
236
+ if (explicitNames.has(normalizedName)) continue;
237
+ const valueBytes = utf8Bytes(value);
238
+ if (!ENV_NAME.test(name) || valueBytes > MAX_ENV_VALUE_LENGTH || value.includes("\0")) {
239
+ if (isExplicitlyInherited) {
240
+ throw new Error("OMP inherited environment contains an invalid value");
241
+ }
242
+ continue;
243
+ }
244
+ if (isExplicitlyInherited && valueBytes > 0 && valueBytes < 4) {
245
+ throw new OmpPublicError("OMP inherited environment value is too short for safe redaction");
246
+ }
247
+ totalBytes += utf8Bytes(name) + valueBytes;
248
+ if (totalBytes > MAX_ENV_TOTAL_LENGTH) {
249
+ throw new Error("OMP inherited environment is too large");
250
+ }
251
+ env[name] = value;
252
+ if (isExplicitlyInherited && valueBytes > 0) inheritedRedactionValues.push(value);
253
+ }
254
+ let entryCount = 0;
255
+ for (const name in sessionEnv ?? {}) {
256
+ if (!Object.hasOwn(sessionEnv ?? {}, name)) continue;
257
+ entryCount += 1;
258
+ if (entryCount > MAX_ENV_ENTRIES)
259
+ throw new Error("OMP session environment has too many entries");
260
+ const value = (sessionEnv as Readonly<Record<string, string>>)[name];
261
+ const normalizedName = name.toUpperCase();
262
+ if (!ENV_NAME.test(name) || BLOCKED_SESSION_ENV.test(normalizedName)) {
263
+ throw new Error("OMP session environment contains a forbidden variable");
264
+ }
265
+ if (
266
+ typeof value !== "string" ||
267
+ utf8Bytes(value) > MAX_ENV_VALUE_LENGTH ||
268
+ value.includes("\0")
269
+ ) {
270
+ throw new Error("OMP session environment contains an invalid value");
271
+ }
272
+ const valueBytes = utf8Bytes(value);
273
+ totalBytes += utf8Bytes(name) + valueBytes;
274
+ if (totalBytes > MAX_ENV_TOTAL_LENGTH) throw new Error("OMP session environment is too large");
275
+ for (const inheritedName of Object.keys(env)) {
276
+ if (inheritedName !== name && inheritedName.toUpperCase() === normalizedName) {
277
+ delete env[inheritedName];
278
+ }
279
+ }
280
+ env[name] = value;
281
+ }
282
+ return { env, inheritedRedactionValues };
283
+ }
284
+
285
+ export function buildOmpSpawnRequest(
286
+ options: OmpStartOptions,
287
+ sourceEnv: NodeJS.ProcessEnv = process.env,
288
+ ): OmpSpawnRequest {
289
+ const environmentSource = options.environment ?? sourceEnv;
290
+ const cwd = validateBoundedText(options.cwd, "working directory", MAX_PATH_LENGTH);
291
+ if (!isAbsolute(cwd)) throw new Error("OMP working directory must be absolute");
292
+ const commandPrefix = options.command ?? [environmentSource.OMP_COMMAND ?? "omp"];
293
+ if (commandPrefix.length === 0) throw new Error("Invalid OMP command");
294
+ const [rawCommand, ...rawPrefixArgs] = commandPrefix;
295
+ const command = validateBoundedText(rawCommand, "command", MAX_PATH_LENGTH);
296
+ const args = rawPrefixArgs.map((argument) =>
297
+ validateBoundedText(argument, "command argument", MAX_PATH_LENGTH),
298
+ );
299
+ if (/[\r\n]/u.test(command)) throw new Error("Invalid OMP command");
300
+ const mode = options.mode ?? "full";
301
+ if (mode !== "full" && mode !== "write" && mode !== "ask") throw new Error("Invalid OMP mode");
302
+ const approvalMode = mode === "full" ? "yolo" : mode === "write" ? "write" : "always-ask";
303
+ if (!args.some((argument) => argument === "--mode" || argument.startsWith("--mode="))) {
304
+ args.push("--mode", "rpc-ui");
305
+ }
306
+ args.push("--approval-mode", approvalMode);
307
+ if (options.tools) {
308
+ if (options.tools.length === 0) args.push("--no-tools");
309
+ else args.push("--tools", options.tools.join(","));
310
+ }
311
+ if (options.model !== undefined) {
312
+ args.push("--model", validateBoundedText(options.model, "model", MAX_MODEL_SELECTOR_BYTES));
313
+ }
314
+ if (options.thinkingOption !== undefined) {
315
+ const thinking = OmpThinkingLevelSchema.safeParse(options.thinkingOption);
316
+ if (!thinking.success) throw new Error("Invalid OMP thinking option");
317
+ args.push("--thinking", thinking.data);
318
+ }
319
+ if (options.roleModels?.smol) {
320
+ args.push(
321
+ "--smol",
322
+ validateBoundedText(options.roleModels.smol, "smol model", MAX_MODEL_SELECTOR_BYTES),
323
+ );
324
+ }
325
+ if (options.roleModels?.slow) {
326
+ args.push(
327
+ "--slow",
328
+ validateBoundedText(options.roleModels.slow, "slow model", MAX_MODEL_SELECTOR_BYTES),
329
+ );
330
+ }
331
+ if (options.roleModels?.plan) {
332
+ args.push(
333
+ "--plan",
334
+ validateBoundedText(options.roleModels.plan, "plan model", MAX_MODEL_SELECTOR_BYTES),
335
+ );
336
+ }
337
+ if (options.sessionDir !== undefined) {
338
+ args.push(
339
+ "--session-dir",
340
+ validateBoundedText(options.sessionDir, "session directory", MAX_PATH_LENGTH),
341
+ );
342
+ }
343
+ if (options.resumeSessionId !== undefined) {
344
+ args.push("--resume", validateNativeSessionId(options.resumeSessionId));
345
+ }
346
+ if (options.noSession) args.push("--no-session");
347
+ const systemPrompt = options.systemPrompt?.trim();
348
+ if (systemPrompt) {
349
+ args.push(
350
+ "--append-system-prompt",
351
+ validateBoundedText(systemPrompt, "system prompt", MAX_SYSTEM_PROMPT_LENGTH),
352
+ );
353
+ }
354
+ const { env, inheritedRedactionValues } = buildOmpEnvironment(
355
+ options.env,
356
+ options.inheritEnv,
357
+ environmentSource,
358
+ );
359
+ env.OMP_NO_WEBP = "1";
360
+ return {
361
+ command,
362
+ args,
363
+ cwd,
364
+ env,
365
+ inheritedRedactionValues,
366
+ detached: process.platform !== "win32",
367
+ };
368
+ }