@omercnet/paseo-omp 0.2.1 → 0.3.0-next.101.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 +7 -3
- 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 +248 -17
- package/server/provider/host-tools.ts +294 -26
- package/server/provider/omp-rpc.ts +499 -72
- package/server/provider/profile-providers.ts +249 -0
- package/server/provider/registration.ts +11 -0
- package/server/provider/security.ts +8 -10
- package/server/provider/session-descriptors.ts +340 -1
- package/server/provider/session.ts +716 -249
- package/server/provider/subsessions.ts +25 -2
- package/server/provider/timeline-projector.ts +104 -44
- 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
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { type Dir, opendirSync } from "node:fs";
|
|
2
|
+
import { opendir } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, join, resolve } from "node:path";
|
|
5
|
+
import { isOmpProfileName } from "../../shared/omp-store";
|
|
6
|
+
import { ompAgentDir, ompCacheDir, ompDataDir, ompSessionDir, ompStateDir } from "../paths";
|
|
7
|
+
import { OmpRpcRuntime, type OmpRuntime, type OmpStartOptions } from "./omp-rpc";
|
|
8
|
+
import { parseOmpProviderOptions } from "./provider-options";
|
|
9
|
+
import { createOmpProvider, type OmpProviderOptions } from "./registration";
|
|
10
|
+
import { OmpPublicError } from "./security";
|
|
11
|
+
|
|
12
|
+
const MAX_PROFILES = 128;
|
|
13
|
+
const MAX_DIRECTORY_ENTRIES = 4_096;
|
|
14
|
+
const STORE_ENV_NAMES: Readonly<Record<string, true>> = {
|
|
15
|
+
OMP_PROFILE: true,
|
|
16
|
+
PI_PROFILE: true,
|
|
17
|
+
PASEO_OMP_AGENT_DIR: true,
|
|
18
|
+
OMP_AGENT_DIR: true,
|
|
19
|
+
PI_CODING_AGENT_DIR: true,
|
|
20
|
+
OMP_SESSION_DIR: true,
|
|
21
|
+
PI_CODING_AGENT_SESSION_DIR: true,
|
|
22
|
+
PI_CONFIG_FILES: true,
|
|
23
|
+
};
|
|
24
|
+
const PROFILE_OVERRIDE_ENV_NAMES: Readonly<Record<string, true>> = {
|
|
25
|
+
...STORE_ENV_NAMES,
|
|
26
|
+
PI_CONFIG_DIR: true,
|
|
27
|
+
HOME: true,
|
|
28
|
+
USERPROFILE: true,
|
|
29
|
+
XDG_DATA_HOME: true,
|
|
30
|
+
XDG_STATE_HOME: true,
|
|
31
|
+
XDG_CACHE_HOME: true,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function validateProfile(profile: string): void {
|
|
35
|
+
if (!isOmpProfileName(profile)) throw new OmpPublicError("Invalid named OMP profile");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function profileDirectory(environment: NodeJS.ProcessEnv): string {
|
|
39
|
+
return join(
|
|
40
|
+
environment.HOME ?? environment.USERPROFILE ?? homedir(),
|
|
41
|
+
environment.PI_CONFIG_DIR || ".omp",
|
|
42
|
+
"profiles",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Paseo 0.8 contribution registration is synchronous; inspect directory names only. */
|
|
47
|
+
export function discoverOmpProfilesSync(environment: NodeJS.ProcessEnv = process.env): string[] {
|
|
48
|
+
let directory: Dir;
|
|
49
|
+
try {
|
|
50
|
+
directory = opendirSync(profileDirectory(environment));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
53
|
+
throw new OmpPublicError("OMP profile directory could not be read");
|
|
54
|
+
}
|
|
55
|
+
const profiles: string[] = [];
|
|
56
|
+
let count = 0;
|
|
57
|
+
try {
|
|
58
|
+
for (let entry = directory.readSync(); entry; entry = directory.readSync()) {
|
|
59
|
+
if (++count > MAX_DIRECTORY_ENTRIES)
|
|
60
|
+
throw new OmpPublicError("OMP profile directory is too large");
|
|
61
|
+
if (entry.isDirectory() && isOmpProfileName(entry.name)) profiles.push(entry.name);
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
directory.closeSync();
|
|
65
|
+
}
|
|
66
|
+
return profiles.sort().slice(0, MAX_PROFILES);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Enumerate names only; never open profile configuration, databases, or credential files. */
|
|
70
|
+
export async function discoverOmpProfiles(
|
|
71
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
72
|
+
): Promise<string[]> {
|
|
73
|
+
let directory: Dir;
|
|
74
|
+
try {
|
|
75
|
+
directory = await opendir(profileDirectory(environment));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
78
|
+
throw new OmpPublicError("OMP profile directory could not be read");
|
|
79
|
+
}
|
|
80
|
+
const profiles: string[] = [];
|
|
81
|
+
let count = 0;
|
|
82
|
+
for await (const entry of directory) {
|
|
83
|
+
if (++count > MAX_DIRECTORY_ENTRIES) {
|
|
84
|
+
throw new OmpPublicError("OMP profile directory is too large");
|
|
85
|
+
}
|
|
86
|
+
if (entry.isDirectory() && isOmpProfileName(entry.name)) profiles.push(entry.name);
|
|
87
|
+
}
|
|
88
|
+
return profiles.sort().slice(0, MAX_PROFILES);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function profileProviderId(profile: string): string {
|
|
92
|
+
validateProfile(profile);
|
|
93
|
+
return `omp-plugin-${profile}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function fixedEnvironment(profile: string, source: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
97
|
+
const environment = { ...source };
|
|
98
|
+
for (const name of Object.keys(environment)) {
|
|
99
|
+
if (STORE_ENV_NAMES[name.toUpperCase()]) delete environment[name];
|
|
100
|
+
}
|
|
101
|
+
environment.OMP_PROFILE = profile;
|
|
102
|
+
environment.PI_CODING_AGENT_DIR = join(profileDirectory(source), profile, "agent");
|
|
103
|
+
return environment;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function profileCommand(
|
|
107
|
+
command: readonly string[],
|
|
108
|
+
profile: string,
|
|
109
|
+
sessionDir: string,
|
|
110
|
+
): readonly string[] {
|
|
111
|
+
// Environment-control flags can erase or replace the fixed profile/config/XDG
|
|
112
|
+
// roots before OMP sees --profile. Allow plain assignment wrappers, not env's
|
|
113
|
+
// -i, -u, -S, -C (or their long forms), including wrappers nested after `--`.
|
|
114
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
115
|
+
const executable = basename(command[index]).toLowerCase();
|
|
116
|
+
if (executable !== "env" && executable !== "env.exe") continue;
|
|
117
|
+
for (const argument of command.slice(index + 1)) {
|
|
118
|
+
if (argument === "--") break;
|
|
119
|
+
if (argument.startsWith("-"))
|
|
120
|
+
throw new OmpPublicError("OMP profile command cannot alter its environment with env flags");
|
|
121
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*=/u.test(argument)) break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
let hasProfile = false;
|
|
125
|
+
for (let index = 1; index < command.length; index += 1) {
|
|
126
|
+
const argument = command[index];
|
|
127
|
+
const assignment = /^([A-Za-z_][A-Za-z0-9_]*)=/u.exec(argument);
|
|
128
|
+
if (assignment && PROFILE_OVERRIDE_ENV_NAMES[assignment[1].toUpperCase()]) {
|
|
129
|
+
throw new OmpPublicError("OMP profile command cannot override its selected store");
|
|
130
|
+
}
|
|
131
|
+
if (argument === "--profile" || argument.startsWith("--profile=")) {
|
|
132
|
+
const selected = argument === "--profile" ? command[++index] : argument.slice(10);
|
|
133
|
+
if (selected !== profile) {
|
|
134
|
+
throw new OmpPublicError("OMP command profile conflicts with the selected provider");
|
|
135
|
+
}
|
|
136
|
+
hasProfile = true;
|
|
137
|
+
} else if (argument === "--session-dir" || argument.startsWith("--session-dir=")) {
|
|
138
|
+
const selected = argument === "--session-dir" ? command[++index] : argument.slice(14);
|
|
139
|
+
if (!selected || resolve(selected) !== sessionDir) {
|
|
140
|
+
throw new OmpPublicError(
|
|
141
|
+
"OMP command session directory conflicts with the selected profile",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return hasProfile ? command : [...command, "--profile", profile];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Each provider owns one profile before the host requests a catalog or creates an agent. */
|
|
150
|
+
export function createProfileOmpProvider(profile: string, options: OmpProviderOptions = {}) {
|
|
151
|
+
const id = profileProviderId(profile);
|
|
152
|
+
const environment = fixedEnvironment(profile, options.environment ?? process.env);
|
|
153
|
+
const sessionDir = resolve(ompSessionDir(environment));
|
|
154
|
+
const runtime = options.runtime ?? new OmpRpcRuntime({ environment });
|
|
155
|
+
const readPersistedSessionTranscript = runtime.readPersistedSessionTranscript?.bind(runtime);
|
|
156
|
+
const assertSessionDir = (requested?: string) => {
|
|
157
|
+
if (requested !== undefined && resolve(requested) !== sessionDir) {
|
|
158
|
+
throw new OmpPublicError("OMP session directory conflicts with the selected profile");
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const assertEnvironment = (sessionEnv?: Readonly<Record<string, string>>) => {
|
|
162
|
+
for (const name of Object.keys(sessionEnv ?? {})) {
|
|
163
|
+
if (PROFILE_OVERRIDE_ENV_NAMES[name.toUpperCase()]) {
|
|
164
|
+
throw new OmpPublicError("OMP session environment cannot override its selected profile");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const scopedRuntime: OmpRuntime = {
|
|
169
|
+
get supportsPersistence() {
|
|
170
|
+
return runtime.supportsPersistence;
|
|
171
|
+
},
|
|
172
|
+
async startSession(start: OmpStartOptions) {
|
|
173
|
+
assertSessionDir(start.sessionDir);
|
|
174
|
+
assertEnvironment(start.env);
|
|
175
|
+
return runtime.startSession({
|
|
176
|
+
...start,
|
|
177
|
+
command: profileCommand(
|
|
178
|
+
start.command ?? [environment.OMP_COMMAND ?? "omp"],
|
|
179
|
+
profile,
|
|
180
|
+
sessionDir,
|
|
181
|
+
),
|
|
182
|
+
environment,
|
|
183
|
+
sessionDir,
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
async listSessions(listOptions) {
|
|
187
|
+
assertSessionDir(listOptions.sessionDir);
|
|
188
|
+
return runtime.listSessions({ ...listOptions, sessionDir });
|
|
189
|
+
},
|
|
190
|
+
...(readPersistedSessionTranscript
|
|
191
|
+
? {
|
|
192
|
+
readPersistedSessionTranscript(input) {
|
|
193
|
+
return readPersistedSessionTranscript(input);
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
: {}),
|
|
197
|
+
readPersistedSubagentTranscript(input) {
|
|
198
|
+
return runtime.readPersistedSubagentTranscript(input);
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
const provider = createOmpProvider({
|
|
202
|
+
...options,
|
|
203
|
+
runtime: scopedRuntime,
|
|
204
|
+
environment,
|
|
205
|
+
catalogIdentity: {
|
|
206
|
+
profile,
|
|
207
|
+
agentRoot: ompAgentDir(environment),
|
|
208
|
+
dataRoot: ompDataDir(environment),
|
|
209
|
+
cacheRoot: ompCacheDir(environment),
|
|
210
|
+
stateRoot: ompStateDir(environment),
|
|
211
|
+
sessionRoot: sessionDir,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
const validateCatalog = (
|
|
215
|
+
input: Parameters<NonNullable<typeof provider.getCatalogCacheKey>>[0],
|
|
216
|
+
) => {
|
|
217
|
+
const parsed = parseOmpProviderOptions(input.providerOptions);
|
|
218
|
+
assertEnvironment(parsed.env);
|
|
219
|
+
assertSessionDir(parsed.params?.sessionDir);
|
|
220
|
+
return profileCommand(
|
|
221
|
+
parsed.command ?? [environment.OMP_COMMAND ?? "omp"],
|
|
222
|
+
profile,
|
|
223
|
+
sessionDir,
|
|
224
|
+
);
|
|
225
|
+
};
|
|
226
|
+
return {
|
|
227
|
+
...provider,
|
|
228
|
+
async getCatalogCacheKey(
|
|
229
|
+
input: Parameters<NonNullable<typeof provider.getCatalogCacheKey>>[0],
|
|
230
|
+
) {
|
|
231
|
+
validateCatalog(input);
|
|
232
|
+
return provider.getCatalogCacheKey?.(input);
|
|
233
|
+
},
|
|
234
|
+
async checkAvailability(
|
|
235
|
+
input: Parameters<NonNullable<typeof provider.checkAvailability>>[0],
|
|
236
|
+
context?: Parameters<NonNullable<typeof provider.checkAvailability>>[1],
|
|
237
|
+
) {
|
|
238
|
+
const command = validateCatalog(input);
|
|
239
|
+
if (!provider.checkAvailability)
|
|
240
|
+
throw new OmpPublicError("OMP availability probe is unavailable");
|
|
241
|
+
return provider.checkAvailability(
|
|
242
|
+
{ ...input, providerOptions: { ...input.providerOptions, command } },
|
|
243
|
+
context,
|
|
244
|
+
);
|
|
245
|
+
},
|
|
246
|
+
id,
|
|
247
|
+
label: `OMP · ${profile}`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import type { ProviderRegistration } from "@getpaseo/plugin/server/provider";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import type { OmpBrowserAuthorizationRegistry } from "../mcp-browser";
|
|
5
6
|
import { probeOmpAvailability } from "../provider-diagnostics";
|
|
6
7
|
import { createOmpConnection, OmpNativeSessionReservations } from "./connection";
|
|
7
8
|
import type { OmpMcpConnector } from "./host-tools";
|
|
@@ -48,6 +49,7 @@ const CAPABILITIES = [
|
|
|
48
49
|
"session.subsession",
|
|
49
50
|
"session.revert.conversation",
|
|
50
51
|
"permission",
|
|
52
|
+
"timeline.plugin",
|
|
51
53
|
] as const;
|
|
52
54
|
const ConnectRequestSchema = z.object({
|
|
53
55
|
versions: z.array(z.number().int().positive().max(16)).min(1).max(8),
|
|
@@ -59,8 +61,11 @@ export interface OmpProviderOptions {
|
|
|
59
61
|
timelineScheduler?: OmpTimelineScheduler;
|
|
60
62
|
replayTimeoutMs?: number;
|
|
61
63
|
environment?: NodeJS.ProcessEnv;
|
|
64
|
+
/** Fixed, non-secret profile/store identity for providers whose configuration is server-owned. */
|
|
65
|
+
catalogIdentity?: Readonly<Record<string, string>>;
|
|
62
66
|
mcpInitializationTimeoutMs?: number;
|
|
63
67
|
mcpConnector?: OmpMcpConnector;
|
|
68
|
+
browserAuthorizationRegistry?: OmpBrowserAuthorizationRegistry;
|
|
64
69
|
availabilityProbe?: (
|
|
65
70
|
options: ProviderCatalogOptionsCompat,
|
|
66
71
|
timeoutMs: number | undefined,
|
|
@@ -90,9 +95,14 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
|
|
|
90
95
|
providerOptionsSchema: OmpProviderOptionsSchema,
|
|
91
96
|
async getCatalogCacheKey(catalogOptions) {
|
|
92
97
|
const providerOptions = parseOmpProviderOptions(catalogOptions.providerOptions);
|
|
98
|
+
// Profile providers cannot safely hash explicit environment values because they may be
|
|
99
|
+
// credentials. Disable sharing for that case; otherwise retain every normalized option
|
|
100
|
+
// that can change discovery alongside the fixed, non-secret store identity.
|
|
101
|
+
if (options.catalogIdentity && Object.keys(providerOptions.env ?? {}).length > 0) return;
|
|
93
102
|
const identity = {
|
|
94
103
|
scope: catalogOptions.scope,
|
|
95
104
|
...(catalogOptions.scope === "workspace" ? { cwd: catalogOptions.cwd } : {}),
|
|
105
|
+
...(options.catalogIdentity ? { store: options.catalogIdentity } : {}),
|
|
96
106
|
providerOptions,
|
|
97
107
|
settings: catalogOptions.settings ?? {},
|
|
98
108
|
defaultCommand: (options.environment ?? process.env).OMP_COMMAND ?? "omp",
|
|
@@ -145,6 +155,7 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
|
|
|
145
155
|
options.mcpConnector,
|
|
146
156
|
options.mcpInitializationTimeoutMs,
|
|
147
157
|
options.replayTimeoutMs,
|
|
158
|
+
options.browserAuthorizationRegistry,
|
|
148
159
|
);
|
|
149
160
|
},
|
|
150
161
|
};
|
|
@@ -83,16 +83,14 @@ function truncateJsonString(value: string, maxBytes: number): string {
|
|
|
83
83
|
export function truncateUtf8(value: string, maxBytes: number): string {
|
|
84
84
|
if (utf8Bytes(value) <= maxBytes) return value;
|
|
85
85
|
const suffix = "<truncated>";
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
return `${output}${suffix}`;
|
|
86
|
+
const suffixBytes = utf8Bytes(suffix);
|
|
87
|
+
if (maxBytes <= 0) return "";
|
|
88
|
+
if (maxBytes < suffixBytes) return suffix.slice(0, Math.floor(maxBytes));
|
|
89
|
+
|
|
90
|
+
const bytes = Buffer.from(value, "utf8");
|
|
91
|
+
let end = Math.min(maxBytes - suffixBytes, bytes.byteLength);
|
|
92
|
+
while (end > 0 && (bytes[end] & 0xc0) === 0x80) end -= 1;
|
|
93
|
+
return `${bytes.subarray(0, end).toString("utf8")}${suffix}`;
|
|
96
94
|
}
|
|
97
95
|
|
|
98
96
|
export interface BoundedJsonMetrics {
|