@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.
- package/README.md +8 -8
- package/client/external-url.ts +2 -11
- package/client/omp-config-styles.ts +284 -0
- package/client/omp-config-surface.tsx +16 -286
- package/client/omp-routing-editor.tsx +1 -3
- package/client/provider-launch-settings.tsx +118 -0
- package/index.client.tsx +21 -2
- package/index.server.ts +10 -0
- package/package.json +14 -9
- package/paseo-plugin.json +2 -1
- package/scripts/prepare-dependencies.mjs +44 -11
- package/server/omp-models.ts +2 -6
- package/server/package-version.ts +1 -1
- package/server/protocol-violation-diagnostics.ts +1 -1
- package/server/provider/catalog.ts +2 -1
- package/server/provider/config-normalization.ts +19 -3
- package/server/provider/connection.ts +30 -0
- package/server/provider/host-tools.ts +2 -2
- package/server/provider/mcp-transport.ts +9 -2
- package/server/provider/omp-rpc-environment.ts +368 -0
- package/server/provider/omp-rpc-process.ts +119 -0
- package/server/provider/omp-rpc-protocol.ts +1949 -0
- package/server/provider/omp-rpc-transport.ts +1373 -0
- package/server/provider/omp-rpc-values.ts +31 -0
- package/server/provider/omp-rpc.ts +267 -3311
- package/server/provider/profile-providers.ts +2 -1
- package/server/provider/prompt-payload.ts +253 -0
- package/server/provider/registration.ts +10 -2
- package/server/provider/session-compaction.ts +187 -0
- package/server/provider/session-descriptors.ts +52 -18
- package/server/provider/session-events.ts +42 -0
- package/server/provider/session-permissions.ts +987 -0
- package/server/provider/session-terminal.ts +307 -0
- package/server/provider/session-usage.ts +234 -0
- package/server/provider/session.ts +145 -1853
- package/server/provider/settings.ts +8 -5
- package/server/provider/subsessions.ts +30 -9
- package/server/provider/timeline-projector.ts +140 -29
- package/shared/external-url.ts +0 -9
- 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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
package/server/omp-models.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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 {
|
|
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
|
-
...(
|
|
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
|
-
...(
|
|
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
|
+
}
|