@omercnet/paseo-omp 0.2.1-next.72.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 +120 -0
- package/SUPPORT.md +42 -0
- package/TESTING.md +150 -0
- package/client/composer-pill-settings.tsx +157 -0
- package/client/hub-icon.tsx +12 -0
- package/client/hub-popover.tsx +132 -0
- package/client/hub-status.ts +29 -0
- package/client/mcp-authorization.tsx +168 -0
- package/client/mcp-popover.tsx +155 -0
- package/client/memory-panel.tsx +76 -0
- package/client/memory-popover.tsx +74 -0
- package/client/omp-config-surface.tsx +1433 -0
- package/client/omp-doc-links.ts +117 -0
- package/client/omp-plugin-manager.tsx +1004 -0
- package/client/omp-store-picker.tsx +89 -0
- package/client/omp-store-state.ts +45 -0
- package/client/provider-diagnostics-state.ts +262 -0
- package/client/provider-icon.tsx +27 -0
- package/client/provider-image.tsx +66 -0
- package/client/quota-popover.tsx +155 -0
- package/client/quota-state.ts +140 -0
- package/client/sessions-popover.tsx +78 -0
- package/docs/alpha-release-checklist.md +68 -0
- package/docs/configuration.md +126 -0
- package/docs/core-provider-issue-audit.md +108 -0
- package/docs/images/mcp-authorization-compact.png +0 -0
- package/docs/images/mcp-controls-wide.png +0 -0
- package/docs/images/plugin-manager.png +0 -0
- package/docs/images/workspace-settings.png +0 -0
- package/docs/installation.md +67 -0
- package/index.client.tsx +488 -0
- package/index.server.ts +81 -0
- package/package.json +84 -0
- package/paseo-plugin.json +5 -0
- package/scripts/prepare-dependencies.mjs +20 -0
- package/server/hub.ts +145 -0
- package/server/mcp-browser.ts +95 -0
- package/server/memory.ts +86 -0
- package/server/mutation-queue.ts +12 -0
- package/server/omp-config.ts +135 -0
- package/server/omp-plugins.ts +676 -0
- package/server/omp-settings.ts +499 -0
- package/server/paths.ts +181 -0
- package/server/provider/catalog.ts +172 -0
- package/server/provider/config-normalization.ts +148 -0
- package/server/provider/connection.ts +1196 -0
- package/server/provider/host-tools.ts +777 -0
- package/server/provider/image.ts +143 -0
- package/server/provider/mcp-transport.ts +394 -0
- package/server/provider/omp-rpc.ts +2806 -0
- package/server/provider/omp.svg +5 -0
- package/server/provider/profile-providers.ts +249 -0
- package/server/provider/provider-options.ts +27 -0
- package/server/provider/registration.ts +162 -0
- package/server/provider/security.ts +317 -0
- package/server/provider/session-descriptors.ts +736 -0
- package/server/provider/session.ts +4796 -0
- package/server/provider/settings.ts +78 -0
- package/server/provider/subsessions.ts +850 -0
- package/server/provider/timeline-projector.ts +1801 -0
- package/server/provider-diagnostics.ts +1143 -0
- package/server/quota.ts +55 -0
- package/server/sessions.ts +58 -0
- package/shared/composer-pill-settings.ts +28 -0
- package/shared/hub.ts +43 -0
- package/shared/mcp.ts +47 -0
- package/shared/memory.ts +24 -0
- package/shared/omp-config.ts +85 -0
- package/shared/omp-plugins.ts +264 -0
- package/shared/omp-settings.ts +214 -0
- package/shared/omp-store.ts +58 -0
- package/shared/provider-diagnostics.ts +126 -0
- package/shared/provider-image.ts +160 -0
- package/shared/quota.ts +23 -0
- package/shared/sessions.ts +24 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { delimiter, dirname, isAbsolute, join } from "node:path";
|
|
5
|
+
import type { RpcInput } from "@getpaseo/plugin";
|
|
6
|
+
import { parseDocument, parse as parseYaml } from "yaml";
|
|
7
|
+
import {
|
|
8
|
+
type listOmpSettings,
|
|
9
|
+
OMP_SETTINGS_CATALOG_VERSION,
|
|
10
|
+
type OmpScalarValue,
|
|
11
|
+
type OmpSetting,
|
|
12
|
+
type OmpSettingType,
|
|
13
|
+
OmpSettingTypeSchema,
|
|
14
|
+
type updateOmpSettings,
|
|
15
|
+
} from "../shared/omp-settings";
|
|
16
|
+
import { SerialMutationQueue } from "./mutation-queue";
|
|
17
|
+
import { readOmpConfigFrom } from "./omp-config";
|
|
18
|
+
import { currentOmpEnvironment } from "./paths";
|
|
19
|
+
import {
|
|
20
|
+
type BoundedRun,
|
|
21
|
+
buildStatefulCommandEnv,
|
|
22
|
+
defaultSpawn,
|
|
23
|
+
resolveExecutablePath,
|
|
24
|
+
runBounded,
|
|
25
|
+
} from "./provider-diagnostics";
|
|
26
|
+
|
|
27
|
+
const MAX_CONFIG_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
28
|
+
const CONFIG_TIMEOUT_MS = 15_000;
|
|
29
|
+
const KILL_GRACE_MS = 1_000;
|
|
30
|
+
const WINDOWS_DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
|
|
31
|
+
const CREDENTIAL_KEY =
|
|
32
|
+
/(?:^|_)(?:API_KEY|ACCESS_KEY|ACCESS_TOKEN|AUTHORIZATION|COOKIE|CREDENTIAL|CREDENTIALS|OAUTH|PASSWORD|PRIVATE_KEY|REFRESH_TOKEN|SECRET|SESSION_TOKEN|TOKEN)(?:$|_)/u;
|
|
33
|
+
|
|
34
|
+
type OmpSettingRecord = {
|
|
35
|
+
value?: unknown;
|
|
36
|
+
redacted?: unknown;
|
|
37
|
+
type?: unknown;
|
|
38
|
+
description?: unknown;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ParsedOmpSettings = { settings: OmpSetting[]; droppedCount: number };
|
|
42
|
+
type CatalogResult = {
|
|
43
|
+
catalogVersion: typeof OMP_SETTINGS_CATALOG_VERSION;
|
|
44
|
+
available: boolean;
|
|
45
|
+
revision?: string;
|
|
46
|
+
droppedCount: number;
|
|
47
|
+
settings: OmpSetting[];
|
|
48
|
+
path?: string;
|
|
49
|
+
error?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type OmpSettingsUpdateResult = {
|
|
53
|
+
conflict: boolean;
|
|
54
|
+
appliedPaths: string[];
|
|
55
|
+
failed?: { path: string; message: string };
|
|
56
|
+
catalog: CatalogResult;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export interface OmpSettingsDependencies {
|
|
60
|
+
resolveExecutable(): Promise<string | null>;
|
|
61
|
+
runConfig(executable: string, args: readonly string[], cwd?: string): Promise<BoundedRun>;
|
|
62
|
+
validateProjectConfig(executable: string, path: string): Promise<BoundedRun>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function readProjectConfigText(path: string): Promise<string> {
|
|
66
|
+
try {
|
|
67
|
+
return await readFile(path, "utf8");
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return "{}\n";
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function runSucceeded(result: BoundedRun): boolean {
|
|
75
|
+
return (
|
|
76
|
+
result.outcome === "exited" &&
|
|
77
|
+
result.exitCode === 0 &&
|
|
78
|
+
result.signal === null &&
|
|
79
|
+
!result.truncated &&
|
|
80
|
+
!result.cleanupFailed
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function writeProjectChanges(
|
|
85
|
+
cwd: string,
|
|
86
|
+
expectedText: string,
|
|
87
|
+
changes: RpcInput<typeof updateOmpSettings>["changes"],
|
|
88
|
+
executable: string,
|
|
89
|
+
dependencies: OmpSettingsDependencies,
|
|
90
|
+
): Promise<"applied" | "conflict"> {
|
|
91
|
+
const path = join(cwd, ".omp", "config.yml");
|
|
92
|
+
try {
|
|
93
|
+
const metadata = await lstat(path);
|
|
94
|
+
if (metadata.isSymbolicLink()) {
|
|
95
|
+
throw new Error("Symlinked workspace OMP configuration cannot be edited through Paseo.");
|
|
96
|
+
}
|
|
97
|
+
if (!metadata.isFile()) throw new Error("Workspace OMP configuration is not a regular file.");
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const snapshot = await readProjectConfigText(path);
|
|
103
|
+
if (snapshot !== expectedText) return "conflict";
|
|
104
|
+
const document = parseDocument(snapshot);
|
|
105
|
+
if (document.errors.length > 0) throw new Error("Workspace OMP configuration is invalid YAML.");
|
|
106
|
+
for (const change of changes) {
|
|
107
|
+
const segments = change.path.split(".");
|
|
108
|
+
if (change.operation === "reset") document.deleteIn(segments);
|
|
109
|
+
else document.setIn(segments, change.value);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await mkdir(dirname(path), { recursive: true });
|
|
113
|
+
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
114
|
+
try {
|
|
115
|
+
await writeFile(temporaryPath, document.toString(), { encoding: "utf8", mode: 0o600 });
|
|
116
|
+
const validation = await dependencies.validateProjectConfig(executable, temporaryPath);
|
|
117
|
+
if (!runSucceeded(validation)) {
|
|
118
|
+
throw new Error("OMP rejected the workspace configuration change.");
|
|
119
|
+
}
|
|
120
|
+
if ((await readProjectConfigText(path)) !== snapshot) return "conflict";
|
|
121
|
+
await rename(temporaryPath, path);
|
|
122
|
+
return "applied";
|
|
123
|
+
} finally {
|
|
124
|
+
await unlink(temporaryPath).catch(() => {});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function hasConfiguredPath(value: unknown, path: string): boolean {
|
|
129
|
+
let current = value;
|
|
130
|
+
for (const segment of path.split(".")) {
|
|
131
|
+
if (current === null || typeof current !== "object" || Array.isArray(current)) return false;
|
|
132
|
+
const record = current as Record<string, unknown>;
|
|
133
|
+
if (!Object.hasOwn(record, segment)) return false;
|
|
134
|
+
current = record[segment];
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function markWorkspaceOverrides(cwd: string, settings: OmpSetting[]): Promise<OmpSetting[]> {
|
|
140
|
+
const directory = join(cwd, ".omp");
|
|
141
|
+
const sources: unknown[] = [];
|
|
142
|
+
try {
|
|
143
|
+
sources.push(JSON.parse(await readFile(join(directory, "settings.json"), "utf8")) as unknown);
|
|
144
|
+
} catch {
|
|
145
|
+
// Legacy project settings are optional.
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
sources.push(parseYaml(await readFile(join(directory, "config.yml"), "utf8")) as unknown);
|
|
149
|
+
} catch {
|
|
150
|
+
// Canonical project settings are optional.
|
|
151
|
+
}
|
|
152
|
+
if (sources.length === 0) return settings;
|
|
153
|
+
return settings.map((setting) =>
|
|
154
|
+
sources.some((source) => hasConfiguredPath(source, setting.path))
|
|
155
|
+
? { ...setting, workspaceOverride: true }
|
|
156
|
+
: setting,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isCredentialSetting(path: string, type: OmpSettingType): boolean {
|
|
161
|
+
if (type !== "string" && type !== "record") return false;
|
|
162
|
+
const normalized = path
|
|
163
|
+
.replace(/([a-z0-9])([A-Z])/gu, "$1_$2")
|
|
164
|
+
.replace(/[^A-Za-z0-9]+/gu, "_")
|
|
165
|
+
.toUpperCase();
|
|
166
|
+
return CREDENTIAL_KEY.test(normalized);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function parseOmpSettingsList(raw: unknown): ParsedOmpSettings {
|
|
170
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
171
|
+
throw new Error("OMP returned an invalid settings document");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const settings: OmpSetting[] = [];
|
|
175
|
+
let droppedCount = 0;
|
|
176
|
+
for (const [path, candidate] of Object.entries(raw)) {
|
|
177
|
+
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
178
|
+
droppedCount += 1;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const entry = candidate as OmpSettingRecord;
|
|
182
|
+
const parsedType = OmpSettingTypeSchema.safeParse(entry.type);
|
|
183
|
+
if (!parsedType.success) {
|
|
184
|
+
droppedCount += 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const redacted = entry.redacted === true || isCredentialSetting(path, parsedType.data);
|
|
189
|
+
const configured = redacted && entry.redacted !== true ? entry.value !== undefined : undefined;
|
|
190
|
+
settings.push({
|
|
191
|
+
path,
|
|
192
|
+
type: parsedType.data,
|
|
193
|
+
description: typeof entry.description === "string" ? entry.description : "",
|
|
194
|
+
...(redacted
|
|
195
|
+
? { redacted: true, ...(configured === undefined ? {} : { configured }) }
|
|
196
|
+
: { value: entry.value }),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return { settings, droppedCount };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function resolveOmpExecutable(): Promise<string | null> {
|
|
203
|
+
return resolveExecutablePath(
|
|
204
|
+
currentOmpEnvironment().OMP_COMMAND ?? "omp",
|
|
205
|
+
(currentOmpEnvironment().PATH ?? "").split(delimiter),
|
|
206
|
+
{
|
|
207
|
+
cwd: process.cwd(),
|
|
208
|
+
platform: process.platform,
|
|
209
|
+
pathExt: currentOmpEnvironment().PATHEXT ?? WINDOWS_DEFAULT_PATHEXT,
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function runOmpConfig(executable: string, args: readonly string[], cwd = process.cwd()) {
|
|
215
|
+
return runBounded(
|
|
216
|
+
defaultSpawn,
|
|
217
|
+
executable,
|
|
218
|
+
["config", ...args],
|
|
219
|
+
buildStatefulCommandEnv(currentOmpEnvironment()),
|
|
220
|
+
CONFIG_TIMEOUT_MS,
|
|
221
|
+
KILL_GRACE_MS,
|
|
222
|
+
MAX_CONFIG_OUTPUT_BYTES,
|
|
223
|
+
cwd,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function validateOmpProjectConfig(executable: string, path: string): Promise<BoundedRun> {
|
|
228
|
+
return runBounded(
|
|
229
|
+
defaultSpawn,
|
|
230
|
+
executable,
|
|
231
|
+
["--config", path, "config", "list", "--json"],
|
|
232
|
+
buildStatefulCommandEnv(currentOmpEnvironment()),
|
|
233
|
+
CONFIG_TIMEOUT_MS,
|
|
234
|
+
KILL_GRACE_MS,
|
|
235
|
+
MAX_CONFIG_OUTPUT_BYTES,
|
|
236
|
+
tmpdir(),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const DEFAULT_DEPENDENCIES: OmpSettingsDependencies = {
|
|
241
|
+
resolveExecutable: resolveOmpExecutable,
|
|
242
|
+
runConfig: runOmpConfig,
|
|
243
|
+
validateProjectConfig: validateOmpProjectConfig,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
async function loadCatalog(
|
|
247
|
+
executable: string | null | undefined,
|
|
248
|
+
dependencies: OmpSettingsDependencies,
|
|
249
|
+
cwd?: string,
|
|
250
|
+
): Promise<CatalogResult> {
|
|
251
|
+
const resolved = executable === undefined ? await dependencies.resolveExecutable() : executable;
|
|
252
|
+
if (!resolved) {
|
|
253
|
+
return {
|
|
254
|
+
catalogVersion: OMP_SETTINGS_CATALOG_VERSION,
|
|
255
|
+
available: false,
|
|
256
|
+
droppedCount: 0,
|
|
257
|
+
settings: [],
|
|
258
|
+
error: "The OMP executable could not be resolved.",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const result = await dependencies.runConfig(resolved, ["list", "--json"], cwd);
|
|
263
|
+
if (result.outcome !== "exited" || result.exitCode !== 0 || result.truncated) {
|
|
264
|
+
return {
|
|
265
|
+
catalogVersion: OMP_SETTINGS_CATALOG_VERSION,
|
|
266
|
+
available: false,
|
|
267
|
+
droppedCount: 0,
|
|
268
|
+
settings: [],
|
|
269
|
+
error:
|
|
270
|
+
result.outcome === "timeout"
|
|
271
|
+
? "OMP settings discovery timed out."
|
|
272
|
+
: "OMP settings discovery failed.",
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let path: string | undefined;
|
|
277
|
+
const pathResult = await dependencies.runConfig(resolved, ["path"], cwd);
|
|
278
|
+
const agentDir =
|
|
279
|
+
pathResult.outcome === "exited" && pathResult.exitCode === 0 ? pathResult.stdout.trim() : "";
|
|
280
|
+
if (agentDir && isAbsolute(agentDir) && !agentDir.includes("\0")) {
|
|
281
|
+
path = (await readOmpConfigFrom(agentDir)).path;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const parsed: unknown = JSON.parse(result.stdout);
|
|
285
|
+
const catalog = parseOmpSettingsList(parsed);
|
|
286
|
+
const projectPath = cwd ? join(cwd, ".omp", "config.yml") : null;
|
|
287
|
+
const projectText = projectPath ? await readProjectConfigText(projectPath) : "";
|
|
288
|
+
const revision = createHash("sha256").update(result.stdout).update(projectText).digest("hex");
|
|
289
|
+
return {
|
|
290
|
+
catalogVersion: OMP_SETTINGS_CATALOG_VERSION,
|
|
291
|
+
available: true,
|
|
292
|
+
revision,
|
|
293
|
+
droppedCount: catalog.droppedCount,
|
|
294
|
+
...(projectPath ? { path: projectPath } : path ? { path } : {}),
|
|
295
|
+
settings: cwd ? await markWorkspaceOverrides(cwd, catalog.settings) : catalog.settings,
|
|
296
|
+
};
|
|
297
|
+
} catch {
|
|
298
|
+
return {
|
|
299
|
+
catalogVersion: OMP_SETTINGS_CATALOG_VERSION,
|
|
300
|
+
available: false,
|
|
301
|
+
droppedCount: 0,
|
|
302
|
+
settings: [],
|
|
303
|
+
error: "OMP returned invalid settings metadata.",
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function serializeScalar(type: OmpSettingType, value: OmpScalarValue): string | null {
|
|
309
|
+
if (type === "boolean") return typeof value === "boolean" ? String(value) : null;
|
|
310
|
+
if (type === "number")
|
|
311
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
|
|
312
|
+
if (type === "string" || type === "enum") return typeof value === "string" ? value : null;
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export async function listOmpSettingsWithDependencies(
|
|
317
|
+
input: RpcInput<typeof listOmpSettings>,
|
|
318
|
+
dependencies: OmpSettingsDependencies,
|
|
319
|
+
): Promise<CatalogResult> {
|
|
320
|
+
if (input.cwd && (!isAbsolute(input.cwd) || input.cwd.includes("\0"))) {
|
|
321
|
+
return {
|
|
322
|
+
catalogVersion: OMP_SETTINGS_CATALOG_VERSION,
|
|
323
|
+
available: false,
|
|
324
|
+
droppedCount: 0,
|
|
325
|
+
settings: [],
|
|
326
|
+
error: "The workspace path is invalid.",
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return loadCatalog(undefined, dependencies, input.cwd);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export async function resolveListOmpSettings(
|
|
333
|
+
input: RpcInput<typeof listOmpSettings>,
|
|
334
|
+
): Promise<CatalogResult> {
|
|
335
|
+
return listOmpSettingsWithDependencies(input, DEFAULT_DEPENDENCIES);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export async function updateOmpSettingsWithDependencies(
|
|
339
|
+
input: RpcInput<typeof updateOmpSettings>,
|
|
340
|
+
dependencies: OmpSettingsDependencies,
|
|
341
|
+
): Promise<OmpSettingsUpdateResult> {
|
|
342
|
+
if (input.cwd && (!isAbsolute(input.cwd) || input.cwd.includes("\0"))) {
|
|
343
|
+
return {
|
|
344
|
+
conflict: false,
|
|
345
|
+
appliedPaths: [],
|
|
346
|
+
failed: { path: "configuration", message: "The workspace path is invalid." },
|
|
347
|
+
catalog: {
|
|
348
|
+
catalogVersion: OMP_SETTINGS_CATALOG_VERSION,
|
|
349
|
+
available: false,
|
|
350
|
+
droppedCount: 0,
|
|
351
|
+
settings: [],
|
|
352
|
+
error: "The workspace path is invalid.",
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const executable = await dependencies.resolveExecutable();
|
|
357
|
+
const current = await loadCatalog(executable, dependencies, input.cwd);
|
|
358
|
+
if (!executable || !current.available || !current.revision) {
|
|
359
|
+
return {
|
|
360
|
+
conflict: false,
|
|
361
|
+
appliedPaths: [],
|
|
362
|
+
failed: {
|
|
363
|
+
path: input.changes[0]?.path ?? "configuration",
|
|
364
|
+
message: current.error ?? "OMP settings are unavailable.",
|
|
365
|
+
},
|
|
366
|
+
catalog: current,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
if (current.revision !== input.revision) {
|
|
370
|
+
return { conflict: true, appliedPaths: [], catalog: current };
|
|
371
|
+
}
|
|
372
|
+
const byPath = new Map(current.settings.map((setting) => [setting.path, setting]));
|
|
373
|
+
if (input.cwd) {
|
|
374
|
+
for (const change of input.changes) {
|
|
375
|
+
const setting = byPath.get(change.path);
|
|
376
|
+
if (
|
|
377
|
+
!setting ||
|
|
378
|
+
setting.redacted ||
|
|
379
|
+
!["boolean", "number", "string", "enum"].includes(setting.type)
|
|
380
|
+
) {
|
|
381
|
+
return {
|
|
382
|
+
conflict: false,
|
|
383
|
+
appliedPaths: [],
|
|
384
|
+
failed: {
|
|
385
|
+
path: change.path,
|
|
386
|
+
message: "This setting cannot be edited as a workspace scalar value.",
|
|
387
|
+
},
|
|
388
|
+
catalog: current,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (change.operation === "set" && serializeScalar(setting.type, change.value) === null) {
|
|
392
|
+
return {
|
|
393
|
+
conflict: false,
|
|
394
|
+
appliedPaths: [],
|
|
395
|
+
failed: { path: change.path, message: `Expected a ${setting.type} value.` },
|
|
396
|
+
catalog: current,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const projectPath = join(input.cwd, ".omp", "config.yml");
|
|
402
|
+
const beforeValidation = await readProjectConfigText(projectPath);
|
|
403
|
+
const verified = await loadCatalog(executable, dependencies, input.cwd);
|
|
404
|
+
const afterValidation = await readProjectConfigText(projectPath);
|
|
405
|
+
if (verified.revision !== input.revision || beforeValidation !== afterValidation) {
|
|
406
|
+
return { conflict: true, appliedPaths: [], catalog: verified };
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
const outcome = await writeProjectChanges(
|
|
410
|
+
input.cwd,
|
|
411
|
+
afterValidation,
|
|
412
|
+
input.changes,
|
|
413
|
+
executable,
|
|
414
|
+
dependencies,
|
|
415
|
+
);
|
|
416
|
+
if (outcome === "conflict") {
|
|
417
|
+
return {
|
|
418
|
+
conflict: true,
|
|
419
|
+
appliedPaths: [],
|
|
420
|
+
catalog: await loadCatalog(executable, dependencies, input.cwd),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
} catch (error) {
|
|
424
|
+
return {
|
|
425
|
+
conflict: false,
|
|
426
|
+
appliedPaths: [],
|
|
427
|
+
failed: {
|
|
428
|
+
path: input.changes[0]?.path ?? "configuration",
|
|
429
|
+
message: error instanceof Error ? error.message : "Could not update workspace settings.",
|
|
430
|
+
},
|
|
431
|
+
catalog: await loadCatalog(executable, dependencies, input.cwd),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
conflict: false,
|
|
436
|
+
appliedPaths: input.changes.map((change) => change.path),
|
|
437
|
+
catalog: await loadCatalog(executable, dependencies, input.cwd),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
const appliedPaths: string[] = [];
|
|
441
|
+
for (const change of input.changes) {
|
|
442
|
+
const setting = byPath.get(change.path);
|
|
443
|
+
if (
|
|
444
|
+
!setting ||
|
|
445
|
+
setting.redacted ||
|
|
446
|
+
!["boolean", "number", "string", "enum"].includes(setting.type)
|
|
447
|
+
) {
|
|
448
|
+
return {
|
|
449
|
+
conflict: false,
|
|
450
|
+
appliedPaths,
|
|
451
|
+
failed: { path: change.path, message: "This setting cannot be edited as a scalar value." },
|
|
452
|
+
catalog: await loadCatalog(executable, dependencies, input.cwd),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
let args: string[] | null;
|
|
456
|
+
if (change.operation === "reset") {
|
|
457
|
+
args = ["reset", change.path];
|
|
458
|
+
} else {
|
|
459
|
+
const value = serializeScalar(setting.type, change.value);
|
|
460
|
+
args = value === null ? null : ["set", change.path, "--json", "--", value];
|
|
461
|
+
}
|
|
462
|
+
if (!args) {
|
|
463
|
+
return {
|
|
464
|
+
conflict: false,
|
|
465
|
+
appliedPaths,
|
|
466
|
+
failed: { path: change.path, message: `Expected a ${setting.type} value.` },
|
|
467
|
+
catalog: appliedPaths.length
|
|
468
|
+
? await loadCatalog(executable, dependencies, input.cwd)
|
|
469
|
+
: current,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
const result = await dependencies.runConfig(executable, args, input.cwd);
|
|
473
|
+
if (result.outcome !== "exited" || result.exitCode !== 0) {
|
|
474
|
+
return {
|
|
475
|
+
conflict: false,
|
|
476
|
+
appliedPaths,
|
|
477
|
+
failed: { path: change.path, message: "OMP rejected this setting change." },
|
|
478
|
+
catalog: await loadCatalog(executable, dependencies, input.cwd),
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
appliedPaths.push(change.path);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
conflict: false,
|
|
486
|
+
appliedPaths,
|
|
487
|
+
catalog: await loadCatalog(executable, dependencies, input.cwd),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const settingsMutationQueue = new SerialMutationQueue();
|
|
492
|
+
|
|
493
|
+
export function resolveUpdateOmpSettings(
|
|
494
|
+
input: RpcInput<typeof updateOmpSettings>,
|
|
495
|
+
): Promise<OmpSettingsUpdateResult> {
|
|
496
|
+
return settingsMutationQueue.run(() =>
|
|
497
|
+
updateOmpSettingsWithDependencies(input, DEFAULT_DEPENDENCIES),
|
|
498
|
+
);
|
|
499
|
+
}
|
package/server/paths.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { closeSync, constants, existsSync, fstatSync, openSync, readSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { isOmpProfileName, type OmpStore, OmpStoreSchema } from "../shared/omp-store";
|
|
6
|
+
|
|
7
|
+
const MAX_SETTINGS_BYTES = 64 * 1024;
|
|
8
|
+
const storeContext = new AsyncLocalStorage<OmpStore>();
|
|
9
|
+
const ServerOmpStoreSchema = OmpStoreSchema.refine(
|
|
10
|
+
(store) => !store.agentDir || isAbsolute(store.agentDir),
|
|
11
|
+
"OMP agent directory must be absolute on the server platform",
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export function withOmpStore<T>(store: OmpStore | undefined, operation: () => T): T {
|
|
15
|
+
return storeContext.run(ServerOmpStoreSchema.parse(store ?? {}), operation);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Per-request selection, never process.env mutation: concurrent profiles stay isolated. */
|
|
19
|
+
export function currentOmpEnvironment(source: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
20
|
+
const store = storeContext.getStore();
|
|
21
|
+
if (!store?.profile && !store?.agentDir) return source;
|
|
22
|
+
const environment = { ...source };
|
|
23
|
+
for (const name of [
|
|
24
|
+
"PASEO_OMP_AGENT_DIR",
|
|
25
|
+
"OMP_AGENT_DIR",
|
|
26
|
+
"PI_CODING_AGENT_DIR",
|
|
27
|
+
"OMP_SESSION_DIR",
|
|
28
|
+
"PI_CODING_AGENT_SESSION_DIR",
|
|
29
|
+
"OMP_PROFILE",
|
|
30
|
+
"PI_PROFILE",
|
|
31
|
+
"PI_CONFIG_FILES",
|
|
32
|
+
])
|
|
33
|
+
delete environment[name];
|
|
34
|
+
if (store.profile) environment.OMP_PROFILE = store.profile;
|
|
35
|
+
else if (store.agentDir) {
|
|
36
|
+
environment.PI_CODING_AGENT_DIR = store.agentDir;
|
|
37
|
+
}
|
|
38
|
+
return environment;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type OmpStorageKind = "data" | "state" | "cache";
|
|
42
|
+
|
|
43
|
+
const XDG_HOME_BY_KIND: Record<OmpStorageKind, string> = {
|
|
44
|
+
data: "XDG_DATA_HOME",
|
|
45
|
+
state: "XDG_STATE_HOME",
|
|
46
|
+
cache: "XDG_CACHE_HOME",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function ompProfile(environment: NodeJS.ProcessEnv): string | undefined {
|
|
50
|
+
const raw =
|
|
51
|
+
environment.OMP_PROFILE !== undefined ? environment.OMP_PROFILE : environment.PI_PROFILE;
|
|
52
|
+
const profile = raw?.trim();
|
|
53
|
+
if (!profile || profile === "default") return;
|
|
54
|
+
if (!isOmpProfileName(profile)) throw new Error("Invalid OMP profile name");
|
|
55
|
+
return profile;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function defaultOmpAgentDir(environment: NodeJS.ProcessEnv, profile?: string): string {
|
|
59
|
+
const home = environment.HOME ?? environment.USERPROFILE ?? homedir();
|
|
60
|
+
return profile
|
|
61
|
+
? join(home, environment.PI_CONFIG_DIR || ".omp", "profiles", profile, "agent")
|
|
62
|
+
: join(home, environment.PI_CONFIG_DIR || ".omp", "agent");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function explicitOmpAgentDir(
|
|
66
|
+
environment: NodeJS.ProcessEnv,
|
|
67
|
+
profile: string | undefined,
|
|
68
|
+
): string | undefined {
|
|
69
|
+
const pluginOverride = environment.PASEO_OMP_AGENT_DIR ?? environment.OMP_AGENT_DIR;
|
|
70
|
+
if (pluginOverride) return pluginOverride;
|
|
71
|
+
// OMP deliberately ignores PI_CODING_AGENT_DIR while a named profile is active.
|
|
72
|
+
if (profile) return;
|
|
73
|
+
const override = environment.PI_CODING_AGENT_DIR;
|
|
74
|
+
const inheritedProfile = environment.PI_PROFILE?.trim();
|
|
75
|
+
// OMP_PROFILE=""/"default" overrides PI_PROFILE, including an agent path
|
|
76
|
+
// propagated by the parent profile. Such a path is not a custom default store.
|
|
77
|
+
if (
|
|
78
|
+
inheritedProfile &&
|
|
79
|
+
isOmpProfileName(inheritedProfile) &&
|
|
80
|
+
override === defaultOmpAgentDir(environment, inheritedProfile)
|
|
81
|
+
)
|
|
82
|
+
return;
|
|
83
|
+
return override ? resolve(override) : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** OMP's configuration agent directory. Data/state/cache may use XDG-specific roots. */
|
|
87
|
+
export function ompAgentDir(environment: NodeJS.ProcessEnv = currentOmpEnvironment()): string {
|
|
88
|
+
const profile = ompProfile(environment);
|
|
89
|
+
return explicitOmpAgentDir(environment, profile) ?? defaultOmpAgentDir(environment, profile);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function ompStorageDir(
|
|
93
|
+
kind: OmpStorageKind,
|
|
94
|
+
environment: NodeJS.ProcessEnv,
|
|
95
|
+
platform: NodeJS.Platform,
|
|
96
|
+
): string {
|
|
97
|
+
const profile = ompProfile(environment);
|
|
98
|
+
const agentDir = ompAgentDir(environment);
|
|
99
|
+
if (
|
|
100
|
+
agentDir !== defaultOmpAgentDir(environment, profile) ||
|
|
101
|
+
(platform !== "linux" && platform !== "darwin")
|
|
102
|
+
) {
|
|
103
|
+
return agentDir;
|
|
104
|
+
}
|
|
105
|
+
const xdgHome = environment[XDG_HOME_BY_KIND[kind]];
|
|
106
|
+
if (!xdgHome) return agentDir;
|
|
107
|
+
const root = profile ? join(xdgHome, "omp", "profiles", profile) : join(xdgHome, "omp");
|
|
108
|
+
return existsSync(root) ? root : agentDir;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function ompDataDir(
|
|
112
|
+
environment: NodeJS.ProcessEnv = currentOmpEnvironment(),
|
|
113
|
+
platform: NodeJS.Platform = process.platform,
|
|
114
|
+
): string {
|
|
115
|
+
return ompStorageDir("data", environment, platform);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function ompCacheDir(
|
|
119
|
+
environment: NodeJS.ProcessEnv = currentOmpEnvironment(),
|
|
120
|
+
platform: NodeJS.Platform = process.platform,
|
|
121
|
+
): string {
|
|
122
|
+
return ompStorageDir("cache", environment, platform);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function ompStateDir(
|
|
126
|
+
environment: NodeJS.ProcessEnv = currentOmpEnvironment(),
|
|
127
|
+
platform: NodeJS.Platform = process.platform,
|
|
128
|
+
): string {
|
|
129
|
+
return ompStorageDir("state", environment, platform);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function configuredSessionDir(agentDir: string): string | undefined {
|
|
133
|
+
for (const settingsPath of [
|
|
134
|
+
join(agentDir, "settings.json"),
|
|
135
|
+
join(agentDir, "..", "settings.json"),
|
|
136
|
+
]) {
|
|
137
|
+
let descriptor: number;
|
|
138
|
+
try {
|
|
139
|
+
descriptor = openSync(
|
|
140
|
+
settingsPath,
|
|
141
|
+
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0),
|
|
142
|
+
);
|
|
143
|
+
} catch {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const stat = fstatSync(descriptor);
|
|
148
|
+
if (!stat.isFile() || stat.size > MAX_SETTINGS_BYTES) continue;
|
|
149
|
+
const buffer = Buffer.allocUnsafe(stat.size);
|
|
150
|
+
const length = readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
151
|
+
const parsed: unknown = JSON.parse(
|
|
152
|
+
new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, length)),
|
|
153
|
+
);
|
|
154
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
155
|
+
const value = (parsed as Record<string, unknown>).sessionDir;
|
|
156
|
+
if (
|
|
157
|
+
typeof value !== "string" ||
|
|
158
|
+
!value ||
|
|
159
|
+
value.includes("\0") ||
|
|
160
|
+
Buffer.byteLength(value) > 4_096
|
|
161
|
+
)
|
|
162
|
+
continue;
|
|
163
|
+
return isAbsolute(value) ? value : resolve(dirname(settingsPath), value);
|
|
164
|
+
} catch {
|
|
165
|
+
} finally {
|
|
166
|
+
closeSync(descriptor);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** OMP's effective session root, honoring its documented environment and settings precedence. */
|
|
173
|
+
export function ompSessionDir(
|
|
174
|
+
environment: NodeJS.ProcessEnv = currentOmpEnvironment(),
|
|
175
|
+
platform: NodeJS.Platform = process.platform,
|
|
176
|
+
): string {
|
|
177
|
+
const explicit = environment.OMP_SESSION_DIR ?? environment.PI_CODING_AGENT_SESSION_DIR;
|
|
178
|
+
if (explicit) return resolve(explicit);
|
|
179
|
+
const agentDir = ompAgentDir(environment);
|
|
180
|
+
return configuredSessionDir(agentDir) ?? join(ompDataDir(environment, platform), "sessions");
|
|
181
|
+
}
|