Skip to content
Merged
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
9 changes: 5 additions & 4 deletions apps/server/src/provider/AntigravityInstallation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,8 +702,9 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => {
source: "managed",
version: previousVersion,
});
const canonicalExternalExecutable = yield* fs.realPath(externalExecutable);
expect(yield* installation.resolve(externalExecutable)).toMatchObject({
executablePath: externalExecutable,
executablePath: canonicalExternalExecutable,
source: "override",
managedVersionDirectory: null,
});
Expand All @@ -724,7 +725,7 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => {
yield* installation.remove();
expect(yield* installation.resolve()).toMatchObject({
source: "path",
executablePath: externalExecutable,
executablePath: canonicalExternalExecutable,
});
const isolated = yield* makeHarness({ baseDir });
expect(yield* isolated.installation.resolve().pipe(Effect.flip)).toMatchObject({
Expand All @@ -734,11 +735,11 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => {
yield* isolated.installation.resolve(undefined, { PATH: externalDirectory }),
).toMatchObject({
source: "path",
executablePath: externalExecutable,
executablePath: canonicalExternalExecutable,
});
expect(
yield* isolated.installation.resolve("agy_acp_server.par", { PATH: externalDirectory }),
).toMatchObject({ source: "override", executablePath: externalExecutable });
).toMatchObject({ source: "override", executablePath: canonicalExternalExecutable });
}),
);

Expand Down
8 changes: 8 additions & 0 deletions apps/server/src/provider/Layers/AntigravityAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,14 @@ it.layer(layer)("AntigravityAdapter", (it) => {
}).pipe(Effect.flip);
expect(escape._tag).toBe("AcpRequestError");
expect(yield* fs.exists(path.join(outside, "escape.txt"))).toBe(false);
yield* fs.symlink(outside, path.join(cwd, "outside-link"));
const symlinkEscape = yield* write({
sessionId: nativeSessionId,
path: path.join(cwd, "outside-link", "nested", "escape.txt"),
content: "nope",
}).pipe(Effect.flip);
expect(symlinkEscape._tag).toBe("AcpRequestError");
expect(yield* fs.exists(path.join(outside, "nested", "escape.txt"))).toBe(false);
const missing = yield* read({
sessionId: nativeSessionId,
path: path.join(cwd, "missing.txt"),
Expand Down
29 changes: 26 additions & 3 deletions apps/server/src/provider/Layers/AntigravityAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,27 @@ function isInsideRoot(path: Path.Path, root: string, candidate: string): boolean
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}

const realPathWithMissingTail = Effect.fn("AntigravityAdapter.realPathWithMissingTail")(
function* (input: {
readonly fileSystem: FileSystem.FileSystem;
readonly path: Path.Path;
readonly candidate: string;
}) {
const missingSegments: Array<string> = [];
let current = input.candidate;
while (true) {
const real = yield* input.fileSystem.realPath(current).pipe(Effect.option);
if (Option.isSome(real)) {
return input.path.join(real.value, ...missingSegments.reverse());
}
const parent = input.path.dirname(current);
if (parent === current) return input.candidate;
missingSegments.push(input.path.basename(current));
current = parent;
}
},
);

/** Resolves an agent-supplied path and rejects anything outside the session roots. */
const resolveClientFilePath = Effect.fn("AntigravityAdapter.resolveClientFilePath")(
function* (input: {
Expand All @@ -207,9 +228,11 @@ const resolveClientFilePath = Effect.fn("AntigravityAdapter.resolveClientFilePat
const { path } = input;
const resolved = path.resolve(input.requestPath);
// Follow symlinks on the parent so a link out of the workspace cannot escape it.
const parent = yield* input.fileSystem
.realPath(path.dirname(resolved))
.pipe(Effect.orElseSucceed(() => path.dirname(resolved)));
const parent = yield* realPathWithMissingTail({
fileSystem: input.fileSystem,
path,
candidate: path.dirname(resolved),
});
const real = path.join(parent, path.basename(resolved));
const roots = yield* Effect.forEach(input.allowedRoots, (root) =>
input.fileSystem.realPath(root).pipe(Effect.orElseSucceed(() => root)),
Expand Down