@omercnet/paseo-omp 0.2.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.
- package/CHANGELOG.md +87 -0
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/SUPPORT.md +40 -0
- package/TESTING.md +147 -0
- package/client/hub-icon.tsx +12 -0
- package/client/hub-popover.tsx +132 -0
- package/client/hub-status.ts +29 -0
- package/client/memory-panel.tsx +71 -0
- package/client/memory-popover.tsx +70 -0
- package/client/omp-config-surface.tsx +1274 -0
- package/client/omp-doc-links.ts +117 -0
- package/client/omp-plugin-manager.tsx +833 -0
- package/client/provider-diagnostics-state.ts +250 -0
- package/client/provider-icon.tsx +27 -0
- package/client/provider-image.tsx +66 -0
- package/client/quota-popover.tsx +150 -0
- package/client/quota-state.ts +131 -0
- package/client/sessions-popover.tsx +73 -0
- package/docs/alpha-release-checklist.md +70 -0
- package/docs/configuration.md +122 -0
- package/docs/core-provider-issue-audit.md +108 -0
- package/docs/installation.md +73 -0
- package/index.client.tsx +272 -0
- package/index.server.ts +51 -0
- package/package.json +84 -0
- package/paseo-plugin.json +5 -0
- package/server/hub.ts +145 -0
- package/server/memory.ts +86 -0
- package/server/mutation-queue.ts +12 -0
- package/server/omp-config.ts +126 -0
- package/server/omp-plugins.ts +627 -0
- package/server/omp-settings.ts +291 -0
- package/server/paths.ts +64 -0
- package/server/provider/catalog.ts +173 -0
- package/server/provider/config-normalization.ts +148 -0
- package/server/provider/connection.ts +992 -0
- package/server/provider/host-tools.ts +706 -0
- package/server/provider/image.ts +143 -0
- package/server/provider/mcp-transport.ts +394 -0
- package/server/provider/omp-rpc.ts +2739 -0
- package/server/provider/omp.svg +5 -0
- package/server/provider/provider-options.ts +27 -0
- package/server/provider/registration.ts +151 -0
- package/server/provider/security.ts +317 -0
- package/server/provider/session-descriptors.ts +431 -0
- package/server/provider/session.ts +4451 -0
- package/server/provider/settings.ts +78 -0
- package/server/provider/subsessions.ts +847 -0
- package/server/provider/timeline-projector.ts +1764 -0
- package/server/provider-diagnostics.ts +1057 -0
- package/server/quota.ts +54 -0
- package/server/sessions.ts +58 -0
- package/shared/hub.ts +43 -0
- package/shared/memory.ts +23 -0
- package/shared/omp-config.ts +81 -0
- package/shared/omp-plugins.ts +223 -0
- package/shared/omp-settings.ts +207 -0
- package/shared/provider-diagnostics.ts +117 -0
- package/shared/provider-image.ts +160 -0
- package/shared/quota.ts +22 -0
- package/shared/sessions.ts +23 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { delimiter, isAbsolute } from "node:path";
|
|
3
|
+
import type { RpcInput } from "@getpaseo/plugin";
|
|
4
|
+
import {
|
|
5
|
+
type listOmpSettings,
|
|
6
|
+
OMP_SETTINGS_CATALOG_VERSION,
|
|
7
|
+
type OmpScalarValue,
|
|
8
|
+
type OmpSetting,
|
|
9
|
+
type OmpSettingType,
|
|
10
|
+
OmpSettingTypeSchema,
|
|
11
|
+
type updateOmpSettings,
|
|
12
|
+
} from "../shared/omp-settings";
|
|
13
|
+
import { SerialMutationQueue } from "./mutation-queue";
|
|
14
|
+
import { readOmpConfigFrom } from "./omp-config";
|
|
15
|
+
import {
|
|
16
|
+
type BoundedRun,
|
|
17
|
+
buildStatefulCommandEnv,
|
|
18
|
+
defaultSpawn,
|
|
19
|
+
resolveExecutablePath,
|
|
20
|
+
runBounded,
|
|
21
|
+
} from "./provider-diagnostics";
|
|
22
|
+
|
|
23
|
+
const MAX_CONFIG_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
24
|
+
const CONFIG_TIMEOUT_MS = 15_000;
|
|
25
|
+
const KILL_GRACE_MS = 1_000;
|
|
26
|
+
const WINDOWS_DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
|
|
27
|
+
const CREDENTIAL_KEY =
|
|
28
|
+
/(?:^|_)(?:API_KEY|ACCESS_KEY|ACCESS_TOKEN|AUTHORIZATION|COOKIE|CREDENTIAL|CREDENTIALS|OAUTH|PASSWORD|PRIVATE_KEY|REFRESH_TOKEN|SECRET|SESSION_TOKEN|TOKEN)(?:$|_)/u;
|
|
29
|
+
|
|
30
|
+
type OmpSettingRecord = {
|
|
31
|
+
value?: unknown;
|
|
32
|
+
redacted?: unknown;
|
|
33
|
+
type?: unknown;
|
|
34
|
+
description?: unknown;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type ParsedOmpSettings = { settings: OmpSetting[]; droppedCount: number };
|
|
38
|
+
type CatalogResult = {
|
|
39
|
+
catalogVersion: typeof OMP_SETTINGS_CATALOG_VERSION;
|
|
40
|
+
available: boolean;
|
|
41
|
+
revision?: string;
|
|
42
|
+
droppedCount: number;
|
|
43
|
+
settings: OmpSetting[];
|
|
44
|
+
path?: string;
|
|
45
|
+
error?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type OmpSettingsUpdateResult = {
|
|
49
|
+
conflict: boolean;
|
|
50
|
+
appliedPaths: string[];
|
|
51
|
+
failed?: { path: string; message: string };
|
|
52
|
+
catalog: CatalogResult;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export interface OmpSettingsDependencies {
|
|
56
|
+
resolveExecutable(): Promise<string | null>;
|
|
57
|
+
runConfig(executable: string, args: readonly string[]): Promise<BoundedRun>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isCredentialSetting(path: string, type: OmpSettingType): boolean {
|
|
61
|
+
if (type !== "string" && type !== "record") return false;
|
|
62
|
+
const normalized = path
|
|
63
|
+
.replace(/([a-z0-9])([A-Z])/gu, "$1_$2")
|
|
64
|
+
.replace(/[^A-Za-z0-9]+/gu, "_")
|
|
65
|
+
.toUpperCase();
|
|
66
|
+
return CREDENTIAL_KEY.test(normalized);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function parseOmpSettingsList(raw: unknown): ParsedOmpSettings {
|
|
70
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
71
|
+
throw new Error("OMP returned an invalid settings document");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const settings: OmpSetting[] = [];
|
|
75
|
+
let droppedCount = 0;
|
|
76
|
+
for (const [path, candidate] of Object.entries(raw)) {
|
|
77
|
+
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
78
|
+
droppedCount += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const entry = candidate as OmpSettingRecord;
|
|
82
|
+
const parsedType = OmpSettingTypeSchema.safeParse(entry.type);
|
|
83
|
+
if (!parsedType.success) {
|
|
84
|
+
droppedCount += 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const redacted = entry.redacted === true || isCredentialSetting(path, parsedType.data);
|
|
89
|
+
const configured = redacted && entry.redacted !== true ? entry.value !== undefined : undefined;
|
|
90
|
+
settings.push({
|
|
91
|
+
path,
|
|
92
|
+
type: parsedType.data,
|
|
93
|
+
description: typeof entry.description === "string" ? entry.description : "",
|
|
94
|
+
...(redacted
|
|
95
|
+
? { redacted: true, ...(configured === undefined ? {} : { configured }) }
|
|
96
|
+
: { value: entry.value }),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return { settings, droppedCount };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function resolveOmpExecutable(): Promise<string | null> {
|
|
103
|
+
return resolveExecutablePath(
|
|
104
|
+
process.env.OMP_COMMAND ?? "omp",
|
|
105
|
+
(process.env.PATH ?? "").split(delimiter),
|
|
106
|
+
{
|
|
107
|
+
cwd: process.cwd(),
|
|
108
|
+
platform: process.platform,
|
|
109
|
+
pathExt: process.env.PATHEXT ?? WINDOWS_DEFAULT_PATHEXT,
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function runOmpConfig(executable: string, args: readonly string[]) {
|
|
115
|
+
return runBounded(
|
|
116
|
+
defaultSpawn,
|
|
117
|
+
executable,
|
|
118
|
+
["config", ...args],
|
|
119
|
+
buildStatefulCommandEnv(process.env),
|
|
120
|
+
CONFIG_TIMEOUT_MS,
|
|
121
|
+
KILL_GRACE_MS,
|
|
122
|
+
MAX_CONFIG_OUTPUT_BYTES,
|
|
123
|
+
process.cwd(),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const DEFAULT_DEPENDENCIES: OmpSettingsDependencies = {
|
|
128
|
+
resolveExecutable: resolveOmpExecutable,
|
|
129
|
+
runConfig: runOmpConfig,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
async function loadCatalog(
|
|
133
|
+
executable: string | null | undefined,
|
|
134
|
+
dependencies: OmpSettingsDependencies,
|
|
135
|
+
): Promise<CatalogResult> {
|
|
136
|
+
const resolved = executable === undefined ? await dependencies.resolveExecutable() : executable;
|
|
137
|
+
if (!resolved) {
|
|
138
|
+
return {
|
|
139
|
+
catalogVersion: OMP_SETTINGS_CATALOG_VERSION,
|
|
140
|
+
available: false,
|
|
141
|
+
droppedCount: 0,
|
|
142
|
+
settings: [],
|
|
143
|
+
error: "The OMP executable could not be resolved.",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const result = await dependencies.runConfig(resolved, ["list", "--json"]);
|
|
148
|
+
if (result.outcome !== "exited" || result.exitCode !== 0 || result.truncated) {
|
|
149
|
+
return {
|
|
150
|
+
catalogVersion: OMP_SETTINGS_CATALOG_VERSION,
|
|
151
|
+
available: false,
|
|
152
|
+
droppedCount: 0,
|
|
153
|
+
settings: [],
|
|
154
|
+
error:
|
|
155
|
+
result.outcome === "timeout"
|
|
156
|
+
? "OMP settings discovery timed out."
|
|
157
|
+
: "OMP settings discovery failed.",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let path: string | undefined;
|
|
162
|
+
const pathResult = await dependencies.runConfig(resolved, ["path"]);
|
|
163
|
+
const agentDir =
|
|
164
|
+
pathResult.outcome === "exited" && pathResult.exitCode === 0 ? pathResult.stdout.trim() : "";
|
|
165
|
+
if (agentDir && isAbsolute(agentDir) && !agentDir.includes("\0")) {
|
|
166
|
+
path = (await readOmpConfigFrom(agentDir)).path;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const parsed: unknown = JSON.parse(result.stdout);
|
|
171
|
+
const catalog = parseOmpSettingsList(parsed);
|
|
172
|
+
return {
|
|
173
|
+
catalogVersion: OMP_SETTINGS_CATALOG_VERSION,
|
|
174
|
+
available: true,
|
|
175
|
+
revision: createHash("sha256").update(result.stdout).digest("hex"),
|
|
176
|
+
droppedCount: catalog.droppedCount,
|
|
177
|
+
...(path ? { path } : {}),
|
|
178
|
+
settings: catalog.settings,
|
|
179
|
+
};
|
|
180
|
+
} catch {
|
|
181
|
+
return {
|
|
182
|
+
catalogVersion: OMP_SETTINGS_CATALOG_VERSION,
|
|
183
|
+
available: false,
|
|
184
|
+
droppedCount: 0,
|
|
185
|
+
settings: [],
|
|
186
|
+
error: "OMP returned invalid settings metadata.",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function serializeScalar(type: OmpSettingType, value: OmpScalarValue): string | null {
|
|
192
|
+
if (type === "boolean") return typeof value === "boolean" ? String(value) : null;
|
|
193
|
+
if (type === "number")
|
|
194
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
|
|
195
|
+
if (type === "string" || type === "enum") return typeof value === "string" ? value : null;
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function listOmpSettingsWithDependencies(
|
|
200
|
+
_input: RpcInput<typeof listOmpSettings>,
|
|
201
|
+
dependencies: OmpSettingsDependencies,
|
|
202
|
+
): Promise<CatalogResult> {
|
|
203
|
+
return loadCatalog(undefined, dependencies);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function resolveListOmpSettings(
|
|
207
|
+
input: RpcInput<typeof listOmpSettings>,
|
|
208
|
+
): Promise<CatalogResult> {
|
|
209
|
+
return listOmpSettingsWithDependencies(input, DEFAULT_DEPENDENCIES);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function updateOmpSettingsWithDependencies(
|
|
213
|
+
input: RpcInput<typeof updateOmpSettings>,
|
|
214
|
+
dependencies: OmpSettingsDependencies,
|
|
215
|
+
): Promise<OmpSettingsUpdateResult> {
|
|
216
|
+
const executable = await dependencies.resolveExecutable();
|
|
217
|
+
const current = await loadCatalog(executable, dependencies);
|
|
218
|
+
if (!executable || !current.available || !current.revision) {
|
|
219
|
+
return {
|
|
220
|
+
conflict: false,
|
|
221
|
+
appliedPaths: [],
|
|
222
|
+
failed: {
|
|
223
|
+
path: input.changes[0]?.path ?? "configuration",
|
|
224
|
+
message: current.error ?? "OMP settings are unavailable.",
|
|
225
|
+
},
|
|
226
|
+
catalog: current,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (current.revision !== input.revision) {
|
|
230
|
+
return { conflict: true, appliedPaths: [], catalog: current };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const byPath = new Map(current.settings.map((setting) => [setting.path, setting]));
|
|
234
|
+
const appliedPaths: string[] = [];
|
|
235
|
+
for (const change of input.changes) {
|
|
236
|
+
const setting = byPath.get(change.path);
|
|
237
|
+
if (
|
|
238
|
+
!setting ||
|
|
239
|
+
setting.redacted ||
|
|
240
|
+
!["boolean", "number", "string", "enum"].includes(setting.type)
|
|
241
|
+
) {
|
|
242
|
+
return {
|
|
243
|
+
conflict: false,
|
|
244
|
+
appliedPaths,
|
|
245
|
+
failed: { path: change.path, message: "This setting cannot be edited as a scalar value." },
|
|
246
|
+
catalog: await loadCatalog(executable, dependencies),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
let args: string[] | null;
|
|
250
|
+
if (change.operation === "reset") {
|
|
251
|
+
args = ["reset", change.path];
|
|
252
|
+
} else {
|
|
253
|
+
const value = serializeScalar(setting.type, change.value);
|
|
254
|
+
args = value === null ? null : ["set", change.path, "--json", "--", value];
|
|
255
|
+
}
|
|
256
|
+
if (!args) {
|
|
257
|
+
return {
|
|
258
|
+
conflict: false,
|
|
259
|
+
appliedPaths,
|
|
260
|
+
failed: { path: change.path, message: `Expected a ${setting.type} value.` },
|
|
261
|
+
catalog: appliedPaths.length ? await loadCatalog(executable, dependencies) : current,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const result = await dependencies.runConfig(executable, args);
|
|
265
|
+
if (result.outcome !== "exited" || result.exitCode !== 0) {
|
|
266
|
+
return {
|
|
267
|
+
conflict: false,
|
|
268
|
+
appliedPaths,
|
|
269
|
+
failed: { path: change.path, message: "OMP rejected this setting change." },
|
|
270
|
+
catalog: await loadCatalog(executable, dependencies),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
appliedPaths.push(change.path);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
conflict: false,
|
|
278
|
+
appliedPaths,
|
|
279
|
+
catalog: await loadCatalog(executable, dependencies),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const settingsMutationQueue = new SerialMutationQueue();
|
|
284
|
+
|
|
285
|
+
export function resolveUpdateOmpSettings(
|
|
286
|
+
input: RpcInput<typeof updateOmpSettings>,
|
|
287
|
+
): Promise<OmpSettingsUpdateResult> {
|
|
288
|
+
return settingsMutationQueue.run(() =>
|
|
289
|
+
updateOmpSettingsWithDependencies(input, DEFAULT_DEPENDENCIES),
|
|
290
|
+
);
|
|
291
|
+
}
|
package/server/paths.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
const MAX_SETTINGS_BYTES = 64 * 1024;
|
|
6
|
+
|
|
7
|
+
/** Root of omp's per-machine agent state (`agent.db`, `history.db`, `memories/`). */
|
|
8
|
+
export function ompAgentDir(environment: NodeJS.ProcessEnv = process.env): string {
|
|
9
|
+
const home = environment.HOME ?? environment.USERPROFILE ?? homedir();
|
|
10
|
+
return (
|
|
11
|
+
environment.PASEO_OMP_AGENT_DIR ??
|
|
12
|
+
environment.OMP_AGENT_DIR ??
|
|
13
|
+
environment.PI_CODING_AGENT_DIR ??
|
|
14
|
+
join(home, environment.PI_CONFIG_DIR ?? ".omp", "agent")
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function configuredSessionDir(agentDir: string): string | undefined {
|
|
19
|
+
for (const settingsPath of [
|
|
20
|
+
join(agentDir, "settings.json"),
|
|
21
|
+
join(agentDir, "..", "settings.json"),
|
|
22
|
+
]) {
|
|
23
|
+
let descriptor: number;
|
|
24
|
+
try {
|
|
25
|
+
descriptor = openSync(
|
|
26
|
+
settingsPath,
|
|
27
|
+
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0),
|
|
28
|
+
);
|
|
29
|
+
} catch {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const stat = fstatSync(descriptor);
|
|
34
|
+
if (!stat.isFile() || stat.size > MAX_SETTINGS_BYTES) continue;
|
|
35
|
+
const buffer = Buffer.allocUnsafe(stat.size);
|
|
36
|
+
const length = readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
37
|
+
const parsed: unknown = JSON.parse(
|
|
38
|
+
new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, length)),
|
|
39
|
+
);
|
|
40
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
41
|
+
const value = (parsed as Record<string, unknown>).sessionDir;
|
|
42
|
+
if (
|
|
43
|
+
typeof value !== "string" ||
|
|
44
|
+
!value ||
|
|
45
|
+
value.includes("\0") ||
|
|
46
|
+
Buffer.byteLength(value) > 4_096
|
|
47
|
+
)
|
|
48
|
+
continue;
|
|
49
|
+
return isAbsolute(value) ? value : resolve(dirname(settingsPath), value);
|
|
50
|
+
} catch {
|
|
51
|
+
} finally {
|
|
52
|
+
closeSync(descriptor);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** OMP's effective session root, honoring its documented environment and settings precedence. */
|
|
59
|
+
export function ompSessionDir(environment: NodeJS.ProcessEnv = process.env): string {
|
|
60
|
+
const explicit = environment.OMP_SESSION_DIR ?? environment.PI_CODING_AGENT_SESSION_DIR;
|
|
61
|
+
if (explicit) return resolve(explicit);
|
|
62
|
+
const agentDir = ompAgentDir(environment);
|
|
63
|
+
return configuredSessionDir(agentDir) ?? join(agentDir, "sessions");
|
|
64
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
ProviderCatalog,
|
|
4
|
+
ProviderMode,
|
|
5
|
+
ProviderModel,
|
|
6
|
+
ProviderThinkingOption,
|
|
7
|
+
} from "@getpaseo/plugin/server/provider";
|
|
8
|
+
import type { NormalizedOmpStartOptions } from "./config-normalization";
|
|
9
|
+
import type { OmpModel, OmpRuntime, OmpRuntimeSession } from "./omp-rpc";
|
|
10
|
+
import {
|
|
11
|
+
configuredOutputRedactionValues,
|
|
12
|
+
OmpCleanupFailure,
|
|
13
|
+
OmpPublicDataSerializer,
|
|
14
|
+
OmpPublicError,
|
|
15
|
+
} from "./security";
|
|
16
|
+
|
|
17
|
+
export const OMP_MODES: readonly ProviderMode[] = [
|
|
18
|
+
{
|
|
19
|
+
id: "full",
|
|
20
|
+
label: "Full Access",
|
|
21
|
+
description: "Launches OMP with yolo approval mode so tools run without prompts.",
|
|
22
|
+
icon: "ShieldOff",
|
|
23
|
+
colorTier: "dangerous",
|
|
24
|
+
isUnattended: true,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "write",
|
|
28
|
+
label: "Write Approval",
|
|
29
|
+
description:
|
|
30
|
+
"Launches OMP with write approval mode; reads are free and writes require approval.",
|
|
31
|
+
icon: "ShieldAlert",
|
|
32
|
+
colorTier: "moderate",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "ask",
|
|
36
|
+
label: "Always Ask",
|
|
37
|
+
description: "Launches OMP with always-ask approval mode for write and exec tools.",
|
|
38
|
+
icon: "ShieldCheck",
|
|
39
|
+
colorTier: "safe",
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const THINKING_OPTIONS: readonly ProviderThinkingOption[] = [
|
|
44
|
+
{ id: "off", label: "Off", description: "No extra reasoning" },
|
|
45
|
+
{ id: "minimal", label: "Minimal", description: "Light reasoning" },
|
|
46
|
+
{ id: "low", label: "Low", description: "Faster reasoning" },
|
|
47
|
+
{ id: "medium", label: "Medium", description: "Balanced reasoning", isDefault: true },
|
|
48
|
+
{ id: "high", label: "High", description: "Deeper reasoning" },
|
|
49
|
+
{ id: "xhigh", label: "XHigh", description: "Extra-high reasoning" },
|
|
50
|
+
{ id: "max", label: "Max", description: "Maximum reasoning" },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export function nativeOmpModelId(model: OmpModel): string {
|
|
54
|
+
if (model.provider.includes("/")) {
|
|
55
|
+
throw new OmpPublicError("OMP reported an invalid model provider");
|
|
56
|
+
}
|
|
57
|
+
return `${model.provider}/${model.id}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function ompModelId(model: OmpModel): string {
|
|
61
|
+
const nativeIdentity = `${Buffer.byteLength(model.provider, "utf8")}:${model.provider}${Buffer.byteLength(model.id, "utf8")}:${model.id}`;
|
|
62
|
+
return `omp:model:${createHash("sha256").update(nativeIdentity).digest("hex")}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function mapOmpModels(
|
|
66
|
+
models: readonly OmpModel[],
|
|
67
|
+
serializer = new OmpPublicDataSerializer(),
|
|
68
|
+
): ProviderModel[] {
|
|
69
|
+
const seenIds = new Map<string, string>();
|
|
70
|
+
return models.map((model) => {
|
|
71
|
+
const thinkingOptions = thinkingForModel(model);
|
|
72
|
+
const id = ompModelId(model);
|
|
73
|
+
const nativeIdentity = nativeOmpModelId(model);
|
|
74
|
+
const existing = seenIds.get(id);
|
|
75
|
+
if (existing !== undefined && existing !== nativeIdentity) {
|
|
76
|
+
throw new Error("OMP model identity collision");
|
|
77
|
+
}
|
|
78
|
+
if (existing !== undefined) throw new Error("OMP reported a duplicate model identity");
|
|
79
|
+
seenIds.set(id, nativeIdentity);
|
|
80
|
+
const provider = serializer.text(model.provider, 256);
|
|
81
|
+
const modelId = serializer.text(model.id, 256);
|
|
82
|
+
const name = model.name ? serializer.text(model.name, 256) : modelId;
|
|
83
|
+
return {
|
|
84
|
+
id,
|
|
85
|
+
label: `${provider}/${name}`,
|
|
86
|
+
description: `${provider}/${modelId}`,
|
|
87
|
+
...(typeof model.contextWindow === "number"
|
|
88
|
+
? { contextWindowMaxTokens: model.contextWindow }
|
|
89
|
+
: {}),
|
|
90
|
+
...(thinkingOptions.length > 0
|
|
91
|
+
? {
|
|
92
|
+
thinkingOptions,
|
|
93
|
+
defaultThinkingOptionId:
|
|
94
|
+
thinkingOptions.find((option) => option.isDefault)?.id ?? thinkingOptions[0]?.id,
|
|
95
|
+
}
|
|
96
|
+
: {}),
|
|
97
|
+
metadata: { provider, modelId },
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function thinkingForModel(model: OmpModel | null | undefined): ProviderThinkingOption[] {
|
|
103
|
+
if (!model?.reasoning) return [];
|
|
104
|
+
const efforts = model.thinking?.efforts ?? [];
|
|
105
|
+
const supported = THINKING_OPTIONS.filter((option) => efforts.includes(option.id));
|
|
106
|
+
const defaultLevel = model.thinking?.defaultLevel;
|
|
107
|
+
const selectedDefault = supported.some((option) => option.id === defaultLevel)
|
|
108
|
+
? defaultLevel
|
|
109
|
+
: supported[0]?.id;
|
|
110
|
+
return supported.map((option) => ({ ...option, isDefault: option.id === selectedDefault }));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function closeCatalogSession(session: OmpRuntimeSession): Promise<void> {
|
|
114
|
+
const cleanup = session.close();
|
|
115
|
+
try {
|
|
116
|
+
await cleanup;
|
|
117
|
+
} catch {
|
|
118
|
+
throw new OmpCleanupFailure("OMP catalog cleanup failed", cleanup);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function discoverOmpCatalog(
|
|
123
|
+
runtime: OmpRuntime,
|
|
124
|
+
options: NormalizedOmpStartOptions,
|
|
125
|
+
signal?: AbortSignal,
|
|
126
|
+
environment?: NodeJS.ProcessEnv,
|
|
127
|
+
): Promise<ProviderCatalog> {
|
|
128
|
+
const session = await runtime.startSession({
|
|
129
|
+
...options,
|
|
130
|
+
signal,
|
|
131
|
+
environment,
|
|
132
|
+
});
|
|
133
|
+
try {
|
|
134
|
+
const [nativeModels, state] = await Promise.all([
|
|
135
|
+
session.getAvailableModels(),
|
|
136
|
+
session.getState(),
|
|
137
|
+
]);
|
|
138
|
+
const configuredValues = configuredOutputRedactionValues(
|
|
139
|
+
options.outputRedaction ?? "none",
|
|
140
|
+
options.env,
|
|
141
|
+
);
|
|
142
|
+
const serializer = new OmpPublicDataSerializer(
|
|
143
|
+
options.outputRedaction === "configured-values"
|
|
144
|
+
? [...configuredValues, ...(session.inheritedRedactionValues ?? [])]
|
|
145
|
+
: configuredValues,
|
|
146
|
+
);
|
|
147
|
+
const models = mapOmpModels(nativeModels, serializer);
|
|
148
|
+
if (models.length === 0) throw new Error("OMP reported no available models");
|
|
149
|
+
const defaultModel = state.model ? ompModelId(state.model) : models[0]?.id;
|
|
150
|
+
const currentModel = state.model
|
|
151
|
+
? nativeModels.find(
|
|
152
|
+
(model) => model.provider === state.model?.provider && model.id === state.model.id,
|
|
153
|
+
)
|
|
154
|
+
: nativeModels[0];
|
|
155
|
+
if (state.model && !currentModel) throw new Error("OMP reported an unadvertised active model");
|
|
156
|
+
const thinkingOptions = thinkingForModel(currentModel);
|
|
157
|
+
const defaultThinkingOption = thinkingOptions.some(
|
|
158
|
+
(option) => option.id === state.thinkingLevel,
|
|
159
|
+
)
|
|
160
|
+
? state.thinkingLevel
|
|
161
|
+
: undefined;
|
|
162
|
+
return {
|
|
163
|
+
models,
|
|
164
|
+
modes: OMP_MODES,
|
|
165
|
+
thinkingOptions,
|
|
166
|
+
defaultModel,
|
|
167
|
+
defaultMode: "full",
|
|
168
|
+
...(defaultThinkingOption ? { defaultThinkingOption } : {}),
|
|
169
|
+
};
|
|
170
|
+
} finally {
|
|
171
|
+
await closeCatalogSession(session);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { ProviderSessionConfig } from "@getpaseo/plugin/server/provider";
|
|
2
|
+
import type { OmpStartOptions } from "./omp-rpc";
|
|
3
|
+
|
|
4
|
+
type ProviderCatalogOptionsCompat = {
|
|
5
|
+
scope: "global" | "workspace";
|
|
6
|
+
cwd?: string;
|
|
7
|
+
force?: boolean;
|
|
8
|
+
providerOptions?: Readonly<Record<string, unknown>>;
|
|
9
|
+
settings?: Readonly<Record<string, unknown>>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
import { parseOmpProviderOptions } from "./provider-options";
|
|
13
|
+
import { OmpPublicError } from "./security";
|
|
14
|
+
import { OmpModeSchema } from "./settings";
|
|
15
|
+
|
|
16
|
+
const OMP_BUILTIN_TOOL_NAMES = [
|
|
17
|
+
"read",
|
|
18
|
+
"bash",
|
|
19
|
+
"edit",
|
|
20
|
+
"ast_grep",
|
|
21
|
+
"ast_edit",
|
|
22
|
+
"ask",
|
|
23
|
+
"debug",
|
|
24
|
+
"eval",
|
|
25
|
+
"github",
|
|
26
|
+
"glob",
|
|
27
|
+
"grep",
|
|
28
|
+
"lsp",
|
|
29
|
+
"checkpoint",
|
|
30
|
+
"rewind",
|
|
31
|
+
"security_scan",
|
|
32
|
+
"task",
|
|
33
|
+
"hub",
|
|
34
|
+
"todo",
|
|
35
|
+
"web_search",
|
|
36
|
+
"write",
|
|
37
|
+
"memory_edit",
|
|
38
|
+
"retain",
|
|
39
|
+
"recall",
|
|
40
|
+
"reflect",
|
|
41
|
+
"learn",
|
|
42
|
+
"manage_skill",
|
|
43
|
+
] as const;
|
|
44
|
+
const OMP_BUILTIN_TOOL_NAME_SET: Readonly<Record<string, true>> = Object.fromEntries(
|
|
45
|
+
OMP_BUILTIN_TOOL_NAMES.map((name) => [name, true]),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
function allowedOmpTools(deniedTools: readonly string[] | undefined): string[] | undefined {
|
|
49
|
+
if (!deniedTools?.length) return;
|
|
50
|
+
const denied = new Set(deniedTools.map((name) => name.trim().toLowerCase()));
|
|
51
|
+
const unsupported = [...denied].filter((name) => OMP_BUILTIN_TOOL_NAME_SET[name] !== true);
|
|
52
|
+
if (unsupported.length > 0) {
|
|
53
|
+
throw new OmpPublicError(
|
|
54
|
+
`OMP cannot enforce unknown denied tools: ${unsupported.sort().join(", ")}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return OMP_BUILTIN_TOOL_NAMES.filter((name) => !denied.has(name));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type NormalizedOmpStartOptions = Omit<OmpStartOptions, "environment" | "signal">;
|
|
61
|
+
export type OmpRecoveryOptions = Omit<OmpStartOptions, "resumeSessionId" | "signal">;
|
|
62
|
+
export function normalizeOmpCatalogOptions(
|
|
63
|
+
options: ProviderCatalogOptionsCompat,
|
|
64
|
+
cwd: string,
|
|
65
|
+
): Omit<OmpStartOptions, "environment" | "signal"> {
|
|
66
|
+
const providerOptions = parseOmpProviderOptions(options.providerOptions);
|
|
67
|
+
const params = providerOptions.params ?? {};
|
|
68
|
+
return {
|
|
69
|
+
cwd,
|
|
70
|
+
mode: "full",
|
|
71
|
+
noSession: true,
|
|
72
|
+
...(providerOptions.command ? { command: providerOptions.command } : {}),
|
|
73
|
+
...(providerOptions.env ? { env: providerOptions.env } : {}),
|
|
74
|
+
...(providerOptions.inheritEnv ? { inheritEnv: providerOptions.inheritEnv } : {}),
|
|
75
|
+
outputRedaction: providerOptions.outputRedaction,
|
|
76
|
+
...(params.sessionDir ? { sessionDir: params.sessionDir } : {}),
|
|
77
|
+
...(params.rpcTimeoutMs
|
|
78
|
+
? { readyTimeoutMs: params.rpcTimeoutMs, requestTimeoutMs: params.rpcTimeoutMs }
|
|
79
|
+
: {}),
|
|
80
|
+
...((params.smolModel || params.slowModel || params.planModel) && {
|
|
81
|
+
roleModels: {
|
|
82
|
+
...(params.smolModel ? { smol: params.smolModel } : {}),
|
|
83
|
+
...(params.slowModel ? { slow: params.slowModel } : {}),
|
|
84
|
+
...(params.planModel ? { plan: params.planModel } : {}),
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Replace only runtime-committed selection fields on an immutable recovery template. */
|
|
90
|
+
export function withCommittedOmpSelection(
|
|
91
|
+
template: OmpRecoveryOptions,
|
|
92
|
+
selection: Readonly<{ model?: string; thinkingOption?: string }>,
|
|
93
|
+
): OmpRecoveryOptions {
|
|
94
|
+
const next = { ...template };
|
|
95
|
+
if (selection.model === undefined) delete next.model;
|
|
96
|
+
else next.model = selection.model;
|
|
97
|
+
if (selection.thinkingOption === undefined) delete next.thinkingOption;
|
|
98
|
+
else next.thinkingOption = selection.thinkingOption;
|
|
99
|
+
return next;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Convert the public plugin session envelope into the native OMP launch contract. */
|
|
103
|
+
export function normalizeOmpSessionConfig(
|
|
104
|
+
config: ProviderSessionConfig,
|
|
105
|
+
permissionSupported = false,
|
|
106
|
+
): NormalizedOmpStartOptions {
|
|
107
|
+
if (Object.keys(config.settings).length > 0) {
|
|
108
|
+
throw new OmpPublicError("OMP does not expose live provider settings");
|
|
109
|
+
}
|
|
110
|
+
const parsedMode = OmpModeSchema.safeParse(config.mode ?? "full");
|
|
111
|
+
if (!parsedMode.success) {
|
|
112
|
+
throw new OmpPublicError(`Unsupported OMP mode '${String(config.mode)}'`);
|
|
113
|
+
}
|
|
114
|
+
if (parsedMode.data !== "full" && !permissionSupported) {
|
|
115
|
+
throw new OmpPublicError(
|
|
116
|
+
`OMP mode '${parsedMode.data}' requires negotiated permission support`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const options = parseOmpProviderOptions(config.providerOptions);
|
|
120
|
+
const deniedTools = (config as ProviderSessionConfig & { deniedTools?: readonly string[] })
|
|
121
|
+
.deniedTools;
|
|
122
|
+
const tools = allowedOmpTools(deniedTools);
|
|
123
|
+
const params = options.params ?? {};
|
|
124
|
+
const env = { ...options.env, ...config.env };
|
|
125
|
+
return {
|
|
126
|
+
cwd: config.cwd,
|
|
127
|
+
...(options.command ? { command: options.command } : {}),
|
|
128
|
+
...(Object.keys(env).length > 0 ? { env } : {}),
|
|
129
|
+
...(options.inheritEnv ? { inheritEnv: options.inheritEnv } : {}),
|
|
130
|
+
outputRedaction: options.outputRedaction,
|
|
131
|
+
mode: parsedMode.data,
|
|
132
|
+
thinkingOption: config.thinkingOption,
|
|
133
|
+
systemPrompt: config.systemPrompt,
|
|
134
|
+
noSession: !config.persist,
|
|
135
|
+
...(params.sessionDir ? { sessionDir: params.sessionDir } : {}),
|
|
136
|
+
...(params.rpcTimeoutMs
|
|
137
|
+
? { readyTimeoutMs: params.rpcTimeoutMs, requestTimeoutMs: params.rpcTimeoutMs }
|
|
138
|
+
: {}),
|
|
139
|
+
...((params.smolModel || params.slowModel || params.planModel) && {
|
|
140
|
+
roleModels: {
|
|
141
|
+
...(params.smolModel ? { smol: params.smolModel } : {}),
|
|
142
|
+
...(params.slowModel ? { slow: params.slowModel } : {}),
|
|
143
|
+
...(params.planModel ? { plan: params.planModel } : {}),
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
...(tools ? { tools } : {}),
|
|
147
|
+
};
|
|
148
|
+
}
|