@omercnet/paseo-omp 0.4.0-next.119.1 → 0.4.0-next.120.1

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.
@@ -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
+ }
@@ -0,0 +1,119 @@
1
+ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
2
+ import { join } from "node:path";
3
+
4
+ type TimerHandle = number | NodeJS.Timeout;
5
+
6
+ export const PROCESS_STOP_TIMEOUT_MS = 750;
7
+ const WINDOWS_DEFAULT_SYSTEM_ROOT = "C:\\Windows";
8
+
9
+ export function waitMs(ms: number): Promise<void> {
10
+ const result = Promise.withResolvers<void>();
11
+ setTimeout(result.resolve, ms);
12
+ return result.promise;
13
+ }
14
+
15
+ function processIsGone(error: unknown): boolean {
16
+ return (error as NodeJS.ErrnoException)?.code === "ESRCH";
17
+ }
18
+
19
+ export function isConfirmedNoProcessSpawnFailure(error: unknown): boolean {
20
+ const code = (error as NodeJS.ErrnoException)?.code;
21
+ return code === "ENOENT" || code === "EACCES" || code === "EPERM";
22
+ }
23
+
24
+ /**
25
+ * Terminates the detached process group created for OMP. This covers descendants that remain in
26
+ * that group after the leader exits; descendants that deliberately re-parent into another process
27
+ * group are outside this transport's containment boundary.
28
+ */
29
+ export async function terminatePosixProcessTree(
30
+ pid: number,
31
+ graceMs: number,
32
+ signalProcess: (pid: number, signal: NodeJS.Signals | 0) => void = process.kill,
33
+ wait: (ms: number) => Promise<void> = waitMs,
34
+ ): Promise<boolean> {
35
+ try {
36
+ signalProcess(-pid, 0);
37
+ } catch (error) {
38
+ return processIsGone(error);
39
+ }
40
+ try {
41
+ signalProcess(-pid, "SIGTERM");
42
+ } catch (error) {
43
+ if (!processIsGone(error)) return false;
44
+ }
45
+ await wait(graceMs);
46
+ try {
47
+ signalProcess(-pid, 0);
48
+ } catch (error) {
49
+ return processIsGone(error);
50
+ }
51
+ try {
52
+ signalProcess(-pid, "SIGKILL");
53
+ } catch (error) {
54
+ if (!processIsGone(error)) return false;
55
+ }
56
+ await wait(graceMs);
57
+ try {
58
+ signalProcess(-pid, 0);
59
+ return false;
60
+ } catch (error) {
61
+ return processIsGone(error);
62
+ }
63
+ }
64
+
65
+ export type ProcessTreeCleanup = "verified" | "uncertain" | "failed";
66
+
67
+ export async function stopWindowsTree(pid: number): Promise<ProcessTreeCleanup> {
68
+ const result = Promise.withResolvers<ProcessTreeCleanup>();
69
+ const systemRoot = process.env.SystemRoot ?? WINDOWS_DEFAULT_SYSTEM_ROOT;
70
+ let taskkill: ChildProcessWithoutNullStreams;
71
+ try {
72
+ taskkill = spawn(
73
+ join(systemRoot, "System32", "taskkill.exe"),
74
+ ["/PID", String(pid), "/T", "/F"],
75
+ {
76
+ stdio: ["pipe", "pipe", "pipe"],
77
+ windowsHide: true,
78
+ env: { SystemRoot: systemRoot },
79
+ },
80
+ );
81
+ } catch {
82
+ return "failed";
83
+ }
84
+ taskkill.stdout.resume();
85
+ taskkill.stderr.resume();
86
+ let settled = false;
87
+ let deadline: TimerHandle | undefined;
88
+ let finalDeadline: TimerHandle | undefined;
89
+ const finish = (outcome: ProcessTreeCleanup) => {
90
+ if (settled) return;
91
+ settled = true;
92
+ clearTimeout(deadline);
93
+ clearTimeout(finalDeadline);
94
+ result.resolve(outcome);
95
+ };
96
+ deadline = setTimeout(() => {
97
+ taskkill.kill("SIGKILL");
98
+ finalDeadline = setTimeout(() => finish("failed"), PROCESS_STOP_TIMEOUT_MS);
99
+ }, PROCESS_STOP_TIMEOUT_MS);
100
+ taskkill.once("error", () => finish("failed"));
101
+ taskkill.once("close", (code, signal) => {
102
+ finish(
103
+ code === 0 && signal === null
104
+ ? "verified"
105
+ : code === 128 && signal === null
106
+ ? "uncertain"
107
+ : "failed",
108
+ );
109
+ });
110
+ return result.promise;
111
+ }
112
+
113
+ export async function terminateSpawnedProcessTree(
114
+ pid: number,
115
+ platform: NodeJS.Platform = process.platform,
116
+ ): Promise<boolean> {
117
+ if (platform === "win32") return (await stopWindowsTree(pid)) === "verified";
118
+ return await terminatePosixProcessTree(pid, PROCESS_STOP_TIMEOUT_MS);
119
+ }
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ import { utf8Bytes } from "./security";
3
+
4
+ export const MAX_ID_LENGTH = 256;
5
+ export const MAX_NAME_LENGTH = 256;
6
+ export const MAX_MODEL_SELECTOR_BYTES = MAX_NAME_LENGTH * 2 + 1;
7
+ export const MAX_TEXT_LENGTH = 1024 * 1024;
8
+ export const MAX_SYSTEM_PROMPT_LENGTH = 64 * 1024;
9
+ export const MAX_PATH_LENGTH = 4_096;
10
+
11
+ export const OmpThinkingLevelSchema = z.enum([
12
+ "off",
13
+ "minimal",
14
+ "low",
15
+ "medium",
16
+ "high",
17
+ "xhigh",
18
+ "max",
19
+ ]);
20
+
21
+ export function validateBoundedText(value: unknown, field: string, maxBytes: number): string {
22
+ if (
23
+ typeof value !== "string" ||
24
+ utf8Bytes(value) === 0 ||
25
+ utf8Bytes(value) > maxBytes ||
26
+ value.includes("\0")
27
+ ) {
28
+ throw new Error(`Invalid OMP ${field}`);
29
+ }
30
+ return value;
31
+ }