@omercnet/paseo-omp 0.2.1 → 0.3.0-next.91.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 +26 -0
- package/README.md +25 -13
- package/SUPPORT.md +6 -2
- package/TESTING.md +21 -18
- package/client/composer-pill-settings.tsx +157 -0
- package/client/external-url.ts +15 -0
- package/client/mcp-authorization.tsx +169 -0
- package/client/mcp-popover.tsx +155 -0
- package/client/memory-panel.tsx +8 -3
- package/client/memory-popover.tsx +8 -4
- package/client/omp-config-surface.tsx +189 -29
- package/client/omp-plugin-manager.tsx +302 -131
- package/client/omp-store-picker.tsx +89 -0
- package/client/omp-store-state.ts +45 -0
- package/client/paseo-types.ts +9 -0
- package/client/provider-diagnostics-state.ts +18 -7
- package/client/quota-popover.tsx +8 -3
- package/client/quota-state.ts +16 -7
- package/client/sessions-popover.tsx +8 -3
- package/docs/alpha-release-checklist.md +6 -8
- package/docs/configuration.md +8 -4
- package/docs/core-provider-issue-audit.md +3 -2
- 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 +35 -19
- package/index.client.tsx +339 -123
- package/index.server.ts +44 -14
- package/package.json +7 -8
- package/paseo-plugin.json +2 -2
- package/scripts/prepare-dependencies.mjs +24 -0
- package/server/mcp-browser.ts +95 -0
- package/server/memory.ts +2 -2
- package/server/omp-config.ts +16 -7
- package/server/omp-plugins.ts +70 -21
- package/server/omp-settings.ts +232 -24
- package/server/paths.ts +128 -11
- package/server/provider/catalog.ts +3 -4
- package/server/provider/connection.ts +213 -9
- package/server/provider/host-tools.ts +71 -0
- package/server/provider/omp-rpc.ts +82 -15
- package/server/provider/profile-providers.ts +249 -0
- package/server/provider/registration.ts +11 -0
- package/server/provider/session-descriptors.ts +306 -1
- package/server/provider/session.ts +704 -249
- package/server/provider/subsessions.ts +4 -1
- package/server/provider/timeline-projector.ts +70 -33
- package/server/provider-diagnostics.ts +122 -36
- package/server/quota.ts +3 -2
- package/server/sessions.ts +2 -2
- package/shared/composer-pill-settings.ts +28 -0
- package/shared/external-url.ts +21 -0
- package/shared/hub.ts +3 -3
- package/shared/mcp.ts +47 -0
- package/shared/memory.ts +2 -1
- package/shared/omp-config.ts +5 -1
- package/shared/omp-plugins.ts +74 -33
- package/shared/omp-settings.ts +8 -1
- package/shared/omp-store.ts +58 -0
- package/shared/provider-diagnostics.ts +12 -3
- package/shared/quota.ts +2 -1
- package/shared/sessions.ts +2 -1
package/server/omp-settings.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
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";
|
|
3
5
|
import type { RpcInput } from "@getpaseo/plugin";
|
|
6
|
+
import { parseDocument, parse as parseYaml } from "yaml";
|
|
4
7
|
import {
|
|
5
8
|
type listOmpSettings,
|
|
6
9
|
OMP_SETTINGS_CATALOG_VERSION,
|
|
@@ -12,6 +15,7 @@ import {
|
|
|
12
15
|
} from "../shared/omp-settings";
|
|
13
16
|
import { SerialMutationQueue } from "./mutation-queue";
|
|
14
17
|
import { readOmpConfigFrom } from "./omp-config";
|
|
18
|
+
import { currentOmpEnvironment } from "./paths";
|
|
15
19
|
import {
|
|
16
20
|
type BoundedRun,
|
|
17
21
|
buildStatefulCommandEnv,
|
|
@@ -54,7 +58,103 @@ export type OmpSettingsUpdateResult = {
|
|
|
54
58
|
|
|
55
59
|
export interface OmpSettingsDependencies {
|
|
56
60
|
resolveExecutable(): Promise<string | null>;
|
|
57
|
-
runConfig(executable: string, args: readonly string[]): Promise<BoundedRun>;
|
|
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
|
+
);
|
|
58
158
|
}
|
|
59
159
|
|
|
60
160
|
function isCredentialSetting(path: string, type: OmpSettingType): boolean {
|
|
@@ -101,37 +201,52 @@ export function parseOmpSettingsList(raw: unknown): ParsedOmpSettings {
|
|
|
101
201
|
|
|
102
202
|
async function resolveOmpExecutable(): Promise<string | null> {
|
|
103
203
|
return resolveExecutablePath(
|
|
104
|
-
|
|
105
|
-
(
|
|
204
|
+
currentOmpEnvironment().OMP_COMMAND ?? "omp",
|
|
205
|
+
(currentOmpEnvironment().PATH ?? "").split(delimiter),
|
|
106
206
|
{
|
|
107
207
|
cwd: process.cwd(),
|
|
108
208
|
platform: process.platform,
|
|
109
|
-
pathExt:
|
|
209
|
+
pathExt: currentOmpEnvironment().PATHEXT ?? WINDOWS_DEFAULT_PATHEXT,
|
|
110
210
|
},
|
|
111
211
|
);
|
|
112
212
|
}
|
|
113
213
|
|
|
114
|
-
async function runOmpConfig(executable: string, args: readonly string[]) {
|
|
214
|
+
async function runOmpConfig(executable: string, args: readonly string[], cwd = process.cwd()) {
|
|
115
215
|
return runBounded(
|
|
116
216
|
defaultSpawn,
|
|
117
217
|
executable,
|
|
118
218
|
["config", ...args],
|
|
119
|
-
buildStatefulCommandEnv(
|
|
219
|
+
buildStatefulCommandEnv(currentOmpEnvironment()),
|
|
120
220
|
CONFIG_TIMEOUT_MS,
|
|
121
221
|
KILL_GRACE_MS,
|
|
122
222
|
MAX_CONFIG_OUTPUT_BYTES,
|
|
123
|
-
|
|
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(),
|
|
124
237
|
);
|
|
125
238
|
}
|
|
126
239
|
|
|
127
240
|
const DEFAULT_DEPENDENCIES: OmpSettingsDependencies = {
|
|
128
241
|
resolveExecutable: resolveOmpExecutable,
|
|
129
242
|
runConfig: runOmpConfig,
|
|
243
|
+
validateProjectConfig: validateOmpProjectConfig,
|
|
130
244
|
};
|
|
131
245
|
|
|
132
246
|
async function loadCatalog(
|
|
133
247
|
executable: string | null | undefined,
|
|
134
248
|
dependencies: OmpSettingsDependencies,
|
|
249
|
+
cwd?: string,
|
|
135
250
|
): Promise<CatalogResult> {
|
|
136
251
|
const resolved = executable === undefined ? await dependencies.resolveExecutable() : executable;
|
|
137
252
|
if (!resolved) {
|
|
@@ -144,7 +259,7 @@ async function loadCatalog(
|
|
|
144
259
|
};
|
|
145
260
|
}
|
|
146
261
|
|
|
147
|
-
const result = await dependencies.runConfig(resolved, ["list", "--json"]);
|
|
262
|
+
const result = await dependencies.runConfig(resolved, ["list", "--json"], cwd);
|
|
148
263
|
if (result.outcome !== "exited" || result.exitCode !== 0 || result.truncated) {
|
|
149
264
|
return {
|
|
150
265
|
catalogVersion: OMP_SETTINGS_CATALOG_VERSION,
|
|
@@ -159,23 +274,25 @@ async function loadCatalog(
|
|
|
159
274
|
}
|
|
160
275
|
|
|
161
276
|
let path: string | undefined;
|
|
162
|
-
const pathResult = await dependencies.runConfig(resolved, ["path"]);
|
|
277
|
+
const pathResult = await dependencies.runConfig(resolved, ["path"], cwd);
|
|
163
278
|
const agentDir =
|
|
164
279
|
pathResult.outcome === "exited" && pathResult.exitCode === 0 ? pathResult.stdout.trim() : "";
|
|
165
280
|
if (agentDir && isAbsolute(agentDir) && !agentDir.includes("\0")) {
|
|
166
281
|
path = (await readOmpConfigFrom(agentDir)).path;
|
|
167
282
|
}
|
|
168
|
-
|
|
169
283
|
try {
|
|
170
284
|
const parsed: unknown = JSON.parse(result.stdout);
|
|
171
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");
|
|
172
289
|
return {
|
|
173
290
|
catalogVersion: OMP_SETTINGS_CATALOG_VERSION,
|
|
174
291
|
available: true,
|
|
175
|
-
revision
|
|
292
|
+
revision,
|
|
176
293
|
droppedCount: catalog.droppedCount,
|
|
177
|
-
...(path ? { path } : {}),
|
|
178
|
-
settings: catalog.settings,
|
|
294
|
+
...(projectPath ? { path: projectPath } : path ? { path } : {}),
|
|
295
|
+
settings: cwd ? await markWorkspaceOverrides(cwd, catalog.settings) : catalog.settings,
|
|
179
296
|
};
|
|
180
297
|
} catch {
|
|
181
298
|
return {
|
|
@@ -197,10 +314,19 @@ function serializeScalar(type: OmpSettingType, value: OmpScalarValue): string |
|
|
|
197
314
|
}
|
|
198
315
|
|
|
199
316
|
export async function listOmpSettingsWithDependencies(
|
|
200
|
-
|
|
317
|
+
input: RpcInput<typeof listOmpSettings>,
|
|
201
318
|
dependencies: OmpSettingsDependencies,
|
|
202
319
|
): Promise<CatalogResult> {
|
|
203
|
-
|
|
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);
|
|
204
330
|
}
|
|
205
331
|
|
|
206
332
|
export async function resolveListOmpSettings(
|
|
@@ -213,8 +339,22 @@ export async function updateOmpSettingsWithDependencies(
|
|
|
213
339
|
input: RpcInput<typeof updateOmpSettings>,
|
|
214
340
|
dependencies: OmpSettingsDependencies,
|
|
215
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
|
+
}
|
|
216
356
|
const executable = await dependencies.resolveExecutable();
|
|
217
|
-
const current = await loadCatalog(executable, dependencies);
|
|
357
|
+
const current = await loadCatalog(executable, dependencies, input.cwd);
|
|
218
358
|
if (!executable || !current.available || !current.revision) {
|
|
219
359
|
return {
|
|
220
360
|
conflict: false,
|
|
@@ -229,8 +369,74 @@ export async function updateOmpSettingsWithDependencies(
|
|
|
229
369
|
if (current.revision !== input.revision) {
|
|
230
370
|
return { conflict: true, appliedPaths: [], catalog: current };
|
|
231
371
|
}
|
|
232
|
-
|
|
233
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
|
+
}
|
|
234
440
|
const appliedPaths: string[] = [];
|
|
235
441
|
for (const change of input.changes) {
|
|
236
442
|
const setting = byPath.get(change.path);
|
|
@@ -243,7 +449,7 @@ export async function updateOmpSettingsWithDependencies(
|
|
|
243
449
|
conflict: false,
|
|
244
450
|
appliedPaths,
|
|
245
451
|
failed: { path: change.path, message: "This setting cannot be edited as a scalar value." },
|
|
246
|
-
catalog: await loadCatalog(executable, dependencies),
|
|
452
|
+
catalog: await loadCatalog(executable, dependencies, input.cwd),
|
|
247
453
|
};
|
|
248
454
|
}
|
|
249
455
|
let args: string[] | null;
|
|
@@ -258,16 +464,18 @@ export async function updateOmpSettingsWithDependencies(
|
|
|
258
464
|
conflict: false,
|
|
259
465
|
appliedPaths,
|
|
260
466
|
failed: { path: change.path, message: `Expected a ${setting.type} value.` },
|
|
261
|
-
catalog: appliedPaths.length
|
|
467
|
+
catalog: appliedPaths.length
|
|
468
|
+
? await loadCatalog(executable, dependencies, input.cwd)
|
|
469
|
+
: current,
|
|
262
470
|
};
|
|
263
471
|
}
|
|
264
|
-
const result = await dependencies.runConfig(executable, args);
|
|
472
|
+
const result = await dependencies.runConfig(executable, args, input.cwd);
|
|
265
473
|
if (result.outcome !== "exited" || result.exitCode !== 0) {
|
|
266
474
|
return {
|
|
267
475
|
conflict: false,
|
|
268
476
|
appliedPaths,
|
|
269
477
|
failed: { path: change.path, message: "OMP rejected this setting change." },
|
|
270
|
-
catalog: await loadCatalog(executable, dependencies),
|
|
478
|
+
catalog: await loadCatalog(executable, dependencies, input.cwd),
|
|
271
479
|
};
|
|
272
480
|
}
|
|
273
481
|
appliedPaths.push(change.path);
|
|
@@ -276,7 +484,7 @@ export async function updateOmpSettingsWithDependencies(
|
|
|
276
484
|
return {
|
|
277
485
|
conflict: false,
|
|
278
486
|
appliedPaths,
|
|
279
|
-
catalog: await loadCatalog(executable, dependencies),
|
|
487
|
+
catalog: await loadCatalog(executable, dependencies, input.cwd),
|
|
280
488
|
};
|
|
281
489
|
}
|
|
282
490
|
|
package/server/paths.ts
CHANGED
|
@@ -1,18 +1,132 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { closeSync, constants, existsSync, fstatSync, openSync, readSync } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { isOmpProfileName, type OmpStore, OmpStoreSchema } from "../shared/omp-store";
|
|
4
6
|
|
|
5
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
|
+
);
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
|
|
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 {
|
|
9
59
|
const home = environment.HOME ?? environment.USERPROFILE ?? homedir();
|
|
10
|
-
return
|
|
11
|
-
environment.
|
|
12
|
-
environment.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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);
|
|
16
130
|
}
|
|
17
131
|
|
|
18
132
|
function configuredSessionDir(agentDir: string): string | undefined {
|
|
@@ -56,9 +170,12 @@ function configuredSessionDir(agentDir: string): string | undefined {
|
|
|
56
170
|
}
|
|
57
171
|
|
|
58
172
|
/** OMP's effective session root, honoring its documented environment and settings precedence. */
|
|
59
|
-
export function ompSessionDir(
|
|
173
|
+
export function ompSessionDir(
|
|
174
|
+
environment: NodeJS.ProcessEnv = currentOmpEnvironment(),
|
|
175
|
+
platform: NodeJS.Platform = process.platform,
|
|
176
|
+
): string {
|
|
60
177
|
const explicit = environment.OMP_SESSION_DIR ?? environment.PI_CODING_AGENT_SESSION_DIR;
|
|
61
178
|
if (explicit) return resolve(explicit);
|
|
62
179
|
const agentDir = ompAgentDir(environment);
|
|
63
|
-
return configuredSessionDir(agentDir) ?? join(
|
|
180
|
+
return configuredSessionDir(agentDir) ?? join(ompDataDir(environment, platform), "sessions");
|
|
64
181
|
}
|
|
@@ -18,7 +18,7 @@ export const OMP_MODES: readonly ProviderMode[] = [
|
|
|
18
18
|
{
|
|
19
19
|
id: "full",
|
|
20
20
|
label: "Full Access",
|
|
21
|
-
description: "
|
|
21
|
+
description: "Runs all tools without approval prompts.",
|
|
22
22
|
icon: "ShieldOff",
|
|
23
23
|
colorTier: "dangerous",
|
|
24
24
|
isUnattended: true,
|
|
@@ -26,15 +26,14 @@ export const OMP_MODES: readonly ProviderMode[] = [
|
|
|
26
26
|
{
|
|
27
27
|
id: "write",
|
|
28
28
|
label: "Write Approval",
|
|
29
|
-
description:
|
|
30
|
-
"Launches OMP with write approval mode; reads are free and writes require approval.",
|
|
29
|
+
description: "Runs reads without approval; writes require approval.",
|
|
31
30
|
icon: "ShieldAlert",
|
|
32
31
|
colorTier: "moderate",
|
|
33
32
|
},
|
|
34
33
|
{
|
|
35
34
|
id: "ask",
|
|
36
35
|
label: "Always Ask",
|
|
37
|
-
description: "
|
|
36
|
+
description: "Requires approval for write and execution tools.",
|
|
38
37
|
icon: "ShieldCheck",
|
|
39
38
|
colorTier: "safe",
|
|
40
39
|
},
|