Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions ts/packages/agents/markdown/src/agent/documentPathPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,21 @@ export function resolveExistingFileWithinRoot(
root: string,
requestedPath: string,
): string | undefined {
const relativePath = normalizeRelativeDocumentPath(requestedPath);
if (relativePath === undefined) {
return undefined;
}
const rootPaths = resolveRootPaths(root);
const candidate = path.resolve(rootPaths.resolvedRoot, requestedPath);
const candidate = path.resolve(rootPaths.resolvedRoot, relativePath);
if (!isPathWithinRoot(rootPaths.resolvedRoot, candidate)) {
return undefined;
}
try {
const canonicalFile = fs.realpathSync(candidate);
return isPathWithinRoot(rootPaths.canonicalRoot, canonicalFile) &&
fs.statSync(canonicalFile).isFile()
? canonicalFile
: undefined;
if (!canonicalFile.startsWith(rootPaths.canonicalRoot + path.sep)) {
return undefined;
}
return fs.statSync(canonicalFile).isFile() ? canonicalFile : undefined;
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
} catch (error) {
if (isFileNotFoundError(error)) {
return undefined;
Expand Down Expand Up @@ -135,7 +139,10 @@ function ensureDirectoryWithinRoot(
}

const canonicalDirectory = fs.realpathSync(nextDirectory);
if (!isPathWithinRoot(root.canonicalRoot, canonicalDirectory)) {
if (
canonicalDirectory !== root.canonicalRoot &&
!canonicalDirectory.startsWith(root.canonicalRoot + path.sep)
) {
return undefined;
}
currentDirectory = canonicalDirectory;
Expand All @@ -147,8 +154,12 @@ export function resolveWritableFileWithinRoot(
root: string,
requestedPath: string,
): string | undefined {
const relativePath = normalizeRelativeDocumentPath(requestedPath);
if (relativePath === undefined) {
return undefined;
}
const rootPaths = resolveRootPaths(root);
const candidate = path.resolve(rootPaths.resolvedRoot, requestedPath);
const candidate = path.resolve(rootPaths.resolvedRoot, relativePath);
if (!isPathWithinRoot(rootPaths.resolvedRoot, candidate)) {
return undefined;
}
Expand All @@ -172,7 +183,7 @@ export function resolveWritableFileWithinRoot(
return undefined;
}
const canonicalFile = fs.realpathSync(writablePath);
return isPathWithinRoot(rootPaths.canonicalRoot, canonicalFile)
return canonicalFile.startsWith(rootPaths.canonicalRoot + path.sep)
? canonicalFile
: undefined;
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ export function persistDocumentOperations(
) {
validateIdentity(binding, expected);
let filePath = resolveBoundFile(binding);
if (!filePath.startsWith(binding.root + path.sep)) {
throw new Error("Document binding path changed");
}
const currentContent = fs.readFileSync(filePath, "utf-8");
const currentRevision = computeContentRevision(currentContent);
if (expected.updatedRevision === currentRevision) {
Expand Down Expand Up @@ -158,6 +161,9 @@ export function persistDocumentOperations(

validateIdentity(binding, expected);
filePath = resolveBoundFile(binding);
if (!filePath.startsWith(binding.root + path.sep)) {
throw new Error("Document binding path changed");
}
if (
computeContentRevision(fs.readFileSync(filePath, "utf-8")) !==
currentRevision
Expand Down
36 changes: 35 additions & 1 deletion ts/packages/agents/markdown/src/agent/ipcTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@

// IPC Message Types for TypeAgent Communication

export interface SetFileMessage {
type: "setFile";
workspaceRoot?: string;
relativePath?: string;
}

export interface BindingUpdatedMessage {
type: "bindingUpdated";
bindingToken: string | null;
boundFilePath: string | null;
boundRoot: string | null;
boundRelativePath: string | null;
}

// Agent ← View: UI command requests
export interface UICommandMessage {
type: "uiCommand";
Expand Down Expand Up @@ -46,14 +60,15 @@ export interface DocumentContentMessage {
type: "documentContent";
requestId: string;
content: string;
source?: "file" | "error";
source?: "client-serializer" | "yjs-fallback" | "file-fallback" | "error";
error?: string;
timestamp: number;
bindingToken: string | null;
boundFilePath: string | null;
boundRoot: string | null;
boundRelativePath: string | null;
revision: string | null;
readToken?: string;
identityMismatch?: boolean;
}

Expand All @@ -68,6 +83,7 @@ export interface LLMOperationsMessage {
expectedRelativePath?: string;
expectedRevision: string;
expectedUpdatedRevision?: string;
expectedReadToken?: string;
}

export interface OperationsAppliedMessage {
Expand Down Expand Up @@ -100,6 +116,24 @@ export interface OperationsAppliedEvent {
operationCount: number;
}

export interface DocumentSnapshotEvent {
type: "documentSnapshot";
bindingToken: string;
markdown: string;
revision: string;
timestamp: number;
}

export interface BindingBootstrapEvent {
type: "bindingBootstrap";
bindingToken: string | null;
documentId: string | null;
documentName: string | null;
boundRelativePath: string | null;
revision: string | null;
timestamp: number;
}

// Client ← View: Markdown content requests
export interface RequestMarkdownMessage {
type: "requestMarkdown";
Expand Down
18 changes: 16 additions & 2 deletions ts/packages/agents/markdown/src/agent/markdownActionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ async function handleStreamingMarkdownAction(
content: markdownContent,
binding,
revision,
readToken,
} = await readCurrentDocumentContent(actionContext);

try {
Expand Down Expand Up @@ -510,6 +511,7 @@ async function handleStreamingMarkdownAction(
binding,
revision,
computeContentRevision(updatedContent),
readToken,
);
}

Expand Down Expand Up @@ -912,6 +914,7 @@ async function readCurrentDocumentContent(
content: string;
binding: CurrentDocumentBinding;
revision: string;
readToken?: string;
}> {
const agentContext = actionContext.sessionContext.agentContext;
const storage = actionContext.sessionContext.sessionStorage;
Expand Down Expand Up @@ -953,6 +956,7 @@ async function readCurrentDocumentContent(
token: agentContext.currentBindingToken,
},
revision: response.revision ?? computeContentRevision(response.content),
...(response.readToken ? { readToken: response.readToken } : {}),
};
}

Expand All @@ -962,6 +966,7 @@ async function applyOperationsForCurrentDocument(
binding: CurrentDocumentBinding,
revision: string,
expectedUpdatedRevision?: string,
expectedReadToken?: string,
): Promise<void> {
const agentContext = actionContext.sessionContext.agentContext;
const storage = actionContext.sessionContext.sessionStorage;
Expand Down Expand Up @@ -1016,6 +1021,7 @@ async function applyOperationsForCurrentDocument(
expectedRelativePath: binding.relativePath,
expectedRevision: revision,
expectedUpdatedRevision,
...(expectedReadToken ? { expectedReadToken } : {}),
};
const viewProcess = getCurrentDocumentViewProcess(agentContext);
if (!viewProcess) {
Expand Down Expand Up @@ -1073,7 +1079,7 @@ async function updateCurrentDocument(
actionContext: ActionContext<MarkdownActionContext>,
agent: Awaited<ReturnType<typeof createMarkdownAgent>>,
): Promise<ActionResult> {
const { content, binding, revision } =
const { content, binding, revision, readToken } =
await readCurrentDocumentContent(actionContext);
const response = await agent.updateDocument(
content,
Expand All @@ -1094,6 +1100,8 @@ async function updateCurrentDocument(
response.data.operations,
binding,
revision,
undefined,
readToken,
);
}
return createActionResult(
Expand Down Expand Up @@ -1155,6 +1163,7 @@ type ApplyExpectations = {
expectedRelativePath: string;
expectedRevision: string;
expectedUpdatedRevision: string | undefined;
expectedReadToken?: string;
};

type ApplyResult = {
Expand Down Expand Up @@ -1189,7 +1198,7 @@ export async function sendOperationsToView(
revisionMismatch: false,
error: "View process operation timeout",
});
}, 15000);
}, 60_000);

const responseHandler = (message: Record<string, unknown>) => {
if (
Expand Down Expand Up @@ -1226,6 +1235,7 @@ type ViewDocumentContentResponse = {
content: string;
bindingToken: string | null;
revision: string | null;
readToken: string | undefined;
identityMismatch: boolean;
error: string | undefined;
};
Expand Down Expand Up @@ -1269,6 +1279,10 @@ export async function getDocumentContentFromView(
typeof message.revision === "string"
? message.revision
: null,
readToken:
typeof message.readToken === "string"
? message.readToken
: undefined,
identityMismatch: message.identityMismatch === true,
error:
typeof message.error === "string"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ export class CollaborationManager {
`Using existing Y.js document: ${documentId} ${filePath ? `(${filePath})` : "(memory-only)"}`,
);
}

forgetDocument(documentId: string): void {
this.documents.delete(documentId);
this.documentPaths.delete(documentId);
debug(`Forgot document: ${documentId}`);
}

getStats(): any {
return {
documents: this.documents.size,
Expand Down
Loading
Loading