@omercnet/paseo-omp 0.4.0-next.114.1 → 0.4.0-next.120.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/client/omp-config-styles.ts +284 -0
- package/client/omp-config-surface.tsx +1 -279
- package/client/omp-routing-editor.tsx +1 -1
- package/package.json +3 -1
- package/server/package-version.ts +1 -1
- package/server/provider/config-normalization.ts +1 -1
- package/server/provider/connection.ts +28 -1
- package/server/provider/mcp-transport.ts +1 -1
- package/server/provider/omp-rpc-environment.ts +368 -0
- package/server/provider/omp-rpc-process.ts +119 -0
- package/server/provider/omp-rpc-values.ts +31 -0
- package/server/provider/omp-rpc.ts +814 -670
- package/server/provider/profile-providers.ts +2 -1
- package/server/provider/prompt-payload.ts +247 -0
- package/server/provider/session-descriptors.ts +52 -18
- package/server/provider/session.ts +23 -244
- package/server/provider/subsessions.ts +28 -6
- package/server/provider/timeline-projector.ts +94 -24
|
@@ -4,7 +4,8 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { basename, join, resolve } from "node:path";
|
|
5
5
|
import { isOmpProfileName } from "../../shared/omp-store";
|
|
6
6
|
import { ompAgentDir, ompCacheDir, ompDataDir, ompSessionDir, ompStateDir } from "../paths";
|
|
7
|
-
import { OmpRpcRuntime, type OmpRuntime
|
|
7
|
+
import { OmpRpcRuntime, type OmpRuntime } from "./omp-rpc";
|
|
8
|
+
import type { OmpStartOptions } from "./omp-rpc-environment";
|
|
8
9
|
import { parseOmpProviderOptions } from "./provider-options";
|
|
9
10
|
import { createOmpProvider, type OmpProviderOptions } from "./registration";
|
|
10
11
|
import { OmpPublicError } from "./security";
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { ProviderContent, ProviderInput } from "@getpaseo/plugin/server/provider";
|
|
2
|
+
import { getForgeDefinitionOrNeutral } from "@getpaseo/protocol/forge-manifest";
|
|
3
|
+
import { isValidImagePayload } from "./image";
|
|
4
|
+
import type { OmpImage } from "./omp-rpc";
|
|
5
|
+
import { OmpPublicError, utf8Bytes } from "./security";
|
|
6
|
+
|
|
7
|
+
type SessionPromptInput = Extract<ProviderInput, { type: "session.prompt" }>;
|
|
8
|
+
const MAX_PROMPT_PARTS = 64;
|
|
9
|
+
export const MAX_PROMPT_TEXT_LENGTH = 1024 * 1024;
|
|
10
|
+
const RPC_REQUEST_ID_BYTES = 36;
|
|
11
|
+
|
|
12
|
+
export function isSafeCommandName(name: string): boolean {
|
|
13
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*(?::[A-Za-z0-9][A-Za-z0-9._-]*)*$/u.test(name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type OmpPromptPayload = { text: string; images: OmpImage[]; commandName?: string };
|
|
17
|
+
|
|
18
|
+
const REVIEW_LINE_MARKERS = { add: "+", remove: "-", context: " " } as const;
|
|
19
|
+
|
|
20
|
+
function renderPromptAttachmentAsText(part: Exclude<ProviderContent, { type: "image" }>): string {
|
|
21
|
+
switch (part.type) {
|
|
22
|
+
case "forge_change_request": {
|
|
23
|
+
return renderChangeRequestAttachment({
|
|
24
|
+
forge: part.forge ?? "github",
|
|
25
|
+
number: part.number,
|
|
26
|
+
title: part.title,
|
|
27
|
+
url: part.url,
|
|
28
|
+
body: part.body,
|
|
29
|
+
projectPath: part.projectPath,
|
|
30
|
+
baseRefName: part.baseRefName,
|
|
31
|
+
headRefName: part.headRefName,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
case "github_pr": {
|
|
35
|
+
return renderChangeRequestAttachment({
|
|
36
|
+
forge: "github",
|
|
37
|
+
number: part.number,
|
|
38
|
+
title: part.title,
|
|
39
|
+
url: part.url,
|
|
40
|
+
body: part.body,
|
|
41
|
+
baseRefName: part.baseRefName,
|
|
42
|
+
headRefName: part.headRefName,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
case "forge_issue": {
|
|
46
|
+
return renderIssueAttachment({
|
|
47
|
+
forge: part.forge ?? "github",
|
|
48
|
+
number: part.number,
|
|
49
|
+
title: part.title,
|
|
50
|
+
url: part.url,
|
|
51
|
+
body: part.body,
|
|
52
|
+
projectPath: part.projectPath,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
case "github_issue": {
|
|
56
|
+
return renderIssueAttachment({
|
|
57
|
+
forge: "github",
|
|
58
|
+
number: part.number,
|
|
59
|
+
title: part.title,
|
|
60
|
+
url: part.url,
|
|
61
|
+
body: part.body,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
case "text": {
|
|
65
|
+
return part.text;
|
|
66
|
+
}
|
|
67
|
+
case "review": {
|
|
68
|
+
const lines = [`Paseo review attachment (${part.mode})`, `CWD: ${part.cwd}`];
|
|
69
|
+
if (part.baseRef) {
|
|
70
|
+
lines.push(`Base: ${part.baseRef}`);
|
|
71
|
+
}
|
|
72
|
+
part.comments.forEach((comment, index) => {
|
|
73
|
+
lines.push(
|
|
74
|
+
"",
|
|
75
|
+
`Comment ${index + 1}: ${comment.filePath}:${comment.side}:${comment.lineNumber}`,
|
|
76
|
+
comment.body,
|
|
77
|
+
comment.context.hunkHeader,
|
|
78
|
+
);
|
|
79
|
+
const target = comment.context.targetLine;
|
|
80
|
+
for (const line of comment.context.lines) {
|
|
81
|
+
const isTarget =
|
|
82
|
+
line.oldLineNumber === target.oldLineNumber &&
|
|
83
|
+
line.newLineNumber === target.newLineNumber &&
|
|
84
|
+
line.type === target.type &&
|
|
85
|
+
line.content === target.content;
|
|
86
|
+
const prefix = isTarget ? "> " : " ";
|
|
87
|
+
const oldLn = padLineNumber(line.oldLineNumber);
|
|
88
|
+
const newLn = padLineNumber(line.newLineNumber);
|
|
89
|
+
lines.push(`${prefix}${oldLn} ${newLn} ${REVIEW_LINE_MARKERS[line.type]}${line.content}`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
}
|
|
94
|
+
case "uploaded_file": {
|
|
95
|
+
return [
|
|
96
|
+
`Uploaded file: ${part.fileName}`,
|
|
97
|
+
`Path: ${part.path}`,
|
|
98
|
+
`MIME: ${part.mimeType}`,
|
|
99
|
+
`Size: ${part.size} bytes`,
|
|
100
|
+
].join("\n");
|
|
101
|
+
}
|
|
102
|
+
default:
|
|
103
|
+
throw new Error("unreachable");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderChangeRequestAttachment(input: {
|
|
108
|
+
forge: string;
|
|
109
|
+
number: number;
|
|
110
|
+
title: string;
|
|
111
|
+
url: string;
|
|
112
|
+
body?: string | null;
|
|
113
|
+
projectPath?: string;
|
|
114
|
+
baseRefName?: string | null;
|
|
115
|
+
headRefName?: string | null;
|
|
116
|
+
}): string {
|
|
117
|
+
const lines = [
|
|
118
|
+
`${formatForgeLabel(input.forge)} ${formatChangeRequestAbbrev(input.forge)} ${formatChangeRequestNumber(input.forge, input.number)}: ${input.title}`,
|
|
119
|
+
input.url,
|
|
120
|
+
];
|
|
121
|
+
if (input.projectPath) {
|
|
122
|
+
lines.push(`Project: ${input.projectPath}`);
|
|
123
|
+
}
|
|
124
|
+
if (input.baseRefName) {
|
|
125
|
+
lines.push(`Base: ${input.baseRefName}`);
|
|
126
|
+
}
|
|
127
|
+
if (input.headRefName) {
|
|
128
|
+
lines.push(`Head: ${input.headRefName}`);
|
|
129
|
+
}
|
|
130
|
+
if (input.body) {
|
|
131
|
+
lines.push("", input.body);
|
|
132
|
+
}
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function renderIssueAttachment(input: {
|
|
137
|
+
forge: string;
|
|
138
|
+
number: number;
|
|
139
|
+
title: string;
|
|
140
|
+
url: string;
|
|
141
|
+
body?: string | null;
|
|
142
|
+
projectPath?: string;
|
|
143
|
+
}): string {
|
|
144
|
+
const lines = [
|
|
145
|
+
`${formatForgeLabel(input.forge)} Issue ${formatIssueNumber(input.forge, input.number)}: ${input.title}`,
|
|
146
|
+
input.url,
|
|
147
|
+
];
|
|
148
|
+
if (input.projectPath) {
|
|
149
|
+
lines.push(`Project: ${input.projectPath}`);
|
|
150
|
+
}
|
|
151
|
+
if (input.body) {
|
|
152
|
+
lines.push("", input.body);
|
|
153
|
+
}
|
|
154
|
+
return lines.join("\n");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatForgeLabel(forge: string): string {
|
|
158
|
+
return getForgeDefinitionOrNeutral(forge).displayName;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function formatChangeRequestAbbrev(forge: string): string {
|
|
162
|
+
return getForgeDefinitionOrNeutral(forge).changeRequestAbbrev;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function formatChangeRequestNumber(forge: string, number: number): string {
|
|
166
|
+
return `${getForgeDefinitionOrNeutral(forge).changeRequestNumberPrefix}${number}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function formatIssueNumber(forge: string, number: number): string {
|
|
170
|
+
return `${getForgeDefinitionOrNeutral(forge).issueNumberPrefix}${number}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function padLineNumber(lineNumber: number | null): string {
|
|
174
|
+
return (lineNumber?.toString() ?? "-").padStart(2);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function promptPayload(input: SessionPromptInput): OmpPromptPayload {
|
|
178
|
+
if (input.prompt.outputSchema !== undefined || input.prompt.clearPendingPermissions) {
|
|
179
|
+
throw new OmpPublicError("OMP does not support structured output or permission controls");
|
|
180
|
+
}
|
|
181
|
+
if (input.prompt.input.type === "command") {
|
|
182
|
+
const name = input.prompt.input.name.trim();
|
|
183
|
+
if (!isSafeCommandName(name)) throw new OmpPublicError("Invalid OMP command name");
|
|
184
|
+
const argumentsText = input.prompt.input.arguments.trim();
|
|
185
|
+
const text = `/${name}${argumentsText ? ` ${argumentsText}` : ""}`;
|
|
186
|
+
if (utf8Bytes(text) > MAX_PROMPT_TEXT_LENGTH)
|
|
187
|
+
throw new OmpPublicError("OMP command is too large");
|
|
188
|
+
return { text, images: [], commandName: name };
|
|
189
|
+
}
|
|
190
|
+
if (input.prompt.input.content.length > MAX_PROMPT_PARTS) {
|
|
191
|
+
throw new OmpPublicError("OMP prompt has too many content parts");
|
|
192
|
+
}
|
|
193
|
+
const parts: string[] = [];
|
|
194
|
+
const images: OmpImage[] = [];
|
|
195
|
+
let length = 0;
|
|
196
|
+
const appendText = (text: string) => {
|
|
197
|
+
length += utf8Bytes(text) + (parts.length > 0 ? 2 : 0);
|
|
198
|
+
if (length > MAX_PROMPT_TEXT_LENGTH) throw new OmpPublicError("OMP prompt is too large");
|
|
199
|
+
parts.push(text);
|
|
200
|
+
};
|
|
201
|
+
for (const part of input.prompt.input.content) {
|
|
202
|
+
if (part.type === "text") {
|
|
203
|
+
appendText(part.text);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (part.type === "image") {
|
|
207
|
+
if (!isValidImagePayload(part.data, part.mimeType, 8 * 1024 * 1024)) {
|
|
208
|
+
throw new OmpPublicError("OMP prompt image is invalid");
|
|
209
|
+
}
|
|
210
|
+
images.push({ type: "image", data: part.data, mimeType: part.mimeType });
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
appendText(renderPromptAttachmentAsText(part));
|
|
214
|
+
}
|
|
215
|
+
const text = parts.join("\n\n").trim();
|
|
216
|
+
if (!text && images.length === 0) throw new OmpPublicError("OMP prompt cannot be empty");
|
|
217
|
+
return { text, images };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function inlinePromptFrameBytes(
|
|
221
|
+
payload: OmpPromptPayload,
|
|
222
|
+
delivery: "prompt" | "steer",
|
|
223
|
+
): number {
|
|
224
|
+
const images = payload.images.map(({ type, mimeType }) => ({ type, data: "", mimeType }));
|
|
225
|
+
const frame = {
|
|
226
|
+
type: delivery,
|
|
227
|
+
message: payload.text,
|
|
228
|
+
...(images.length > 0 ? { images } : {}),
|
|
229
|
+
...(delivery === "prompt" ? { id: "" } : {}),
|
|
230
|
+
};
|
|
231
|
+
const requestIdBytes = delivery === "prompt" ? RPC_REQUEST_ID_BYTES : 0;
|
|
232
|
+
return (
|
|
233
|
+
utf8Bytes(JSON.stringify(frame)) +
|
|
234
|
+
requestIdBytes +
|
|
235
|
+
payload.images.reduce((total, image) => total + image.data.length, 0) +
|
|
236
|
+
1
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function slashCommandName(text: string): string | undefined {
|
|
241
|
+
if (!text.startsWith("/")) return undefined;
|
|
242
|
+
const body = text.slice(1);
|
|
243
|
+
if (!body) return undefined;
|
|
244
|
+
const firstWhitespace = body.search(/\s/u);
|
|
245
|
+
const name = firstWhitespace === -1 ? body : body.slice(0, firstWhitespace);
|
|
246
|
+
return name || undefined;
|
|
247
|
+
}
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { constants, type Dir } from "node:fs";
|
|
3
3
|
import { type FileHandle, lstat, open, opendir, realpath } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { basename,
|
|
5
|
+
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { ompSessionDir } from "../paths";
|
|
7
7
|
import { isValidImagePayload } from "./image";
|
|
8
8
|
|
|
@@ -510,16 +510,19 @@ interface PersistedTranscriptNode {
|
|
|
510
510
|
message?: unknown;
|
|
511
511
|
}
|
|
512
512
|
|
|
513
|
-
function persistedTranscriptMessage(record: Record<string, unknown>, entryId
|
|
513
|
+
function persistedTranscriptMessage(record: Record<string, unknown>, entryId?: string): unknown {
|
|
514
514
|
if (record.type === "message") {
|
|
515
515
|
if (!record.message || typeof record.message !== "object" || Array.isArray(record.message))
|
|
516
516
|
return;
|
|
517
|
-
return {
|
|
517
|
+
return {
|
|
518
|
+
...(record.message as Record<string, unknown>),
|
|
519
|
+
...(entryId ? { entryId } : {}),
|
|
520
|
+
};
|
|
518
521
|
}
|
|
519
522
|
if (record.type !== "custom_message") return;
|
|
520
523
|
return {
|
|
521
524
|
role: "custom",
|
|
522
|
-
entryId,
|
|
525
|
+
...(entryId ? { entryId } : {}),
|
|
523
526
|
customType: record.customType,
|
|
524
527
|
content: record.content,
|
|
525
528
|
display: record.display,
|
|
@@ -680,6 +683,7 @@ export async function readOmpPersistedSubagentTranscript(
|
|
|
680
683
|
childTranscriptId: string,
|
|
681
684
|
cwd: string,
|
|
682
685
|
signal?: AbortSignal,
|
|
686
|
+
childSessionFile?: string,
|
|
683
687
|
): Promise<OmpPersistedSubagentTranscript> {
|
|
684
688
|
signal?.throwIfAborted();
|
|
685
689
|
if (
|
|
@@ -687,7 +691,12 @@ export async function readOmpPersistedSubagentTranscript(
|
|
|
687
691
|
!parentSessionFile.endsWith(".jsonl") ||
|
|
688
692
|
parentSessionFile.includes("\0") ||
|
|
689
693
|
!CHILD_TRANSCRIPT_ID.test(childTranscriptId) ||
|
|
690
|
-
basename(childTranscriptId) !== childTranscriptId
|
|
694
|
+
basename(childTranscriptId) !== childTranscriptId ||
|
|
695
|
+
(childSessionFile !== undefined &&
|
|
696
|
+
(!isAbsolute(childSessionFile) ||
|
|
697
|
+
!childSessionFile.endsWith(".jsonl") ||
|
|
698
|
+
childSessionFile.includes("\0") ||
|
|
699
|
+
basename(childSessionFile) !== `${childTranscriptId}.jsonl`))
|
|
691
700
|
) {
|
|
692
701
|
throw new Error("Invalid OMP child transcript descriptor");
|
|
693
702
|
}
|
|
@@ -707,34 +716,53 @@ export async function readOmpPersistedSubagentTranscript(
|
|
|
707
716
|
await parentHandle.close().catch(() => undefined);
|
|
708
717
|
}
|
|
709
718
|
const canonicalParent = await realpath(parentSessionFile);
|
|
710
|
-
const
|
|
711
|
-
const expectedDirectory = canonicalParent.slice(0, -parentExtension.length);
|
|
719
|
+
const expectedDirectory = canonicalParent.slice(0, -".jsonl".length);
|
|
712
720
|
const canonicalDirectory = await realpath(expectedDirectory).catch(() => undefined);
|
|
713
721
|
if (!canonicalDirectory || canonicalDirectory !== expectedDirectory) {
|
|
714
722
|
throw new Error("OMP child transcript directory is not canonically owned by its parent");
|
|
715
723
|
}
|
|
716
|
-
const
|
|
724
|
+
const requestedFile = resolve(
|
|
725
|
+
childSessionFile ?? join(canonicalDirectory, `${childTranscriptId}.jsonl`),
|
|
726
|
+
);
|
|
717
727
|
let handle: FileHandle;
|
|
718
728
|
try {
|
|
719
729
|
handle = await open(
|
|
720
|
-
|
|
730
|
+
requestedFile,
|
|
721
731
|
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0),
|
|
722
732
|
);
|
|
723
733
|
} catch {
|
|
724
734
|
throw new Error("OMP child transcript could not be opened");
|
|
725
735
|
}
|
|
726
736
|
try {
|
|
727
|
-
const [stat, canonicalChild] = await Promise.all([
|
|
737
|
+
const [stat, pathStat, canonicalChild] = await Promise.all([
|
|
738
|
+
handle.stat(),
|
|
739
|
+
lstat(requestedFile),
|
|
740
|
+
realpath(requestedFile),
|
|
741
|
+
]);
|
|
728
742
|
if (
|
|
729
743
|
!stat.isFile() ||
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
744
|
+
!pathStat.isFile() ||
|
|
745
|
+
pathStat.isSymbolicLink() ||
|
|
746
|
+
stat.dev !== pathStat.dev ||
|
|
747
|
+
stat.ino !== pathStat.ino ||
|
|
748
|
+
stat.size > MAX_CHILD_TRANSCRIPT_BYTES
|
|
733
749
|
) {
|
|
734
750
|
throw new Error("OMP child transcript failed ownership validation");
|
|
735
751
|
}
|
|
736
|
-
const
|
|
737
|
-
|
|
752
|
+
const canonicalRelativeChild = relative(canonicalDirectory, canonicalChild);
|
|
753
|
+
if (
|
|
754
|
+
!canonicalRelativeChild ||
|
|
755
|
+
canonicalRelativeChild.startsWith("..") ||
|
|
756
|
+
isAbsolute(canonicalRelativeChild)
|
|
757
|
+
) {
|
|
758
|
+
throw new Error("OMP child transcript failed ownership validation");
|
|
759
|
+
}
|
|
760
|
+
const bytes = await readStableFile(
|
|
761
|
+
handle,
|
|
762
|
+
stat.size,
|
|
763
|
+
signal,
|
|
764
|
+
"OMP child transcript changed while reading",
|
|
765
|
+
);
|
|
738
766
|
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
739
767
|
const messages: unknown[] = [];
|
|
740
768
|
let nativeSessionId: string | undefined;
|
|
@@ -752,15 +780,21 @@ export async function readOmpPersistedSubagentTranscript(
|
|
|
752
780
|
nativeSessionId ??= candidateId;
|
|
753
781
|
if (nativeSessionId !== candidateId)
|
|
754
782
|
throw new Error("OMP child transcript identity changed");
|
|
755
|
-
} else
|
|
783
|
+
} else {
|
|
784
|
+
const entryId =
|
|
785
|
+
typeof record.id === "string" && CHILD_TRANSCRIPT_ID.test(record.id)
|
|
786
|
+
? record.id
|
|
787
|
+
: undefined;
|
|
788
|
+
const message = persistedTranscriptMessage(record, entryId);
|
|
789
|
+
if (message === undefined) continue;
|
|
756
790
|
if (messages.length >= MAX_CHILD_TRANSCRIPT_MESSAGES) {
|
|
757
791
|
throw new Error("OMP child transcript exceeds message limits");
|
|
758
792
|
}
|
|
759
|
-
messages.push(
|
|
793
|
+
messages.push(message);
|
|
760
794
|
}
|
|
761
795
|
}
|
|
762
796
|
if (!nativeSessionId) throw new Error("OMP child transcript is missing session identity");
|
|
763
|
-
return { sessionFile, nativeSessionId, byteLength: bytes.byteLength, messages };
|
|
797
|
+
return { sessionFile: canonicalChild, nativeSessionId, byteLength: bytes.byteLength, messages };
|
|
764
798
|
} catch (error) {
|
|
765
799
|
if (error instanceof Error) throw error;
|
|
766
800
|
throw new Error("OMP child transcript could not be decoded");
|