AkurAI Build
Menu

popagent

public

Latest change 2a1c2af1710141efbd59375b5c19ab958cf1b5c5 - Fix contained workspace LSP inspection by Ólafur Búi Ólafsson

import { LocalFilesystem, LocalSandbox, Workspace, WORKSPACE_TOOLS } from "@mastra/core/workspace";
import { realpathSync } from "node:fs";
import { access } from "node:fs/promises";
import { isAbsolute, join, relative, resolve } from "node:path";
import { DEFAULT_WORKSPACE_ID, type AgentWorkspace } from "./api-types";
import {
  agentWorkspaces,
  type AgentWorkspaceStore,
  WORKSPACE_ROOT,
} from "./agent-workspaces";

const SELF_UPDATE_WORKSPACE_ROOT = resolve(
  process.env.POPAGENT_DATA_DIR ?? join(process.cwd(), "data"),
  "self-update",
);

const DEFAULT_WORKSPACE_PATH = `${WORKSPACE_ROOT}/${process.env.POPAGENT_DEFAULT_REPOSITORY ?? "."}`;


const APPLICATION_NODE_MODULES = resolve("node_modules");
const NODE_EXECUTABLE = realpathSync(process.execPath);
const TYPESCRIPT_LANGUAGE_SERVER = join(
  APPLICATION_NODE_MODULES,
  "typescript-language-server",
  "lib",
  "cli.mjs",
);
class ContainedWorkspaceFilesystem extends LocalFilesystem {
  override exists(inputPath: string): Promise<boolean> {
    if (!isAbsolute(inputPath)) return super.exists(inputPath);
    const containedPath = relative(this.basePath, inputPath);
    if (containedPath === ".." || containedPath.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
      return Promise.resolve(false);
    }
    return super.exists(containedPath || ".");
  }
}


function createWorkspace(
  record: AgentWorkspace,
  path: string,
  readOnly: boolean,
  sandboxReadOnlyPaths: string[] = [],
): Workspace {
  const readOnlyPaths = [...new Set([
    APPLICATION_NODE_MODULES,
    NODE_EXECUTABLE,
    ...sandboxReadOnlyPaths,
    ...(readOnly ? [path] : []),
  ])];
  return new Workspace({
    id: `popagent-${record.id}-${readOnly ? "read-only" : "writable"}`,
    name: `${record.name}${readOnly ? " (read only)" : ""}`,
    filesystem: new ContainedWorkspaceFilesystem({
      basePath: path,
      contained: true,
      readOnly,
      instructions: ({ defaultInstructions }) => [
        defaultInstructions,
        "Use repository-relative paths for every workspace tool, including LSP inspection. Never pass sandbox-visible absolute paths such as /workspace/repository/file.ts.",
      ].join("\n"),
    }),
    sandbox: new LocalSandbox({
      workingDirectory: path,
      isolation: "bwrap" as const,
      timeout: 30_000,
      nativeSandbox: {
        allowNetwork: false,
        readOnlyPaths,
      },
    }),
    lsp: {
      root: path,
      initTimeout: 30_000,
      diagnosticTimeout: 10_000,
      binaryOverrides: {
        typescript: `${NODE_EXECUTABLE} ${TYPESCRIPT_LANGUAGE_SERVER} --stdio`,
      },
    },
    tools: readOnly ? {
      [WORKSPACE_TOOLS.FILESYSTEM.WRITE_FILE]: { enabled: false },
      [WORKSPACE_TOOLS.FILESYSTEM.EDIT_FILE]: { enabled: false },
      [WORKSPACE_TOOLS.FILESYSTEM.DELETE]: { enabled: false },
      [WORKSPACE_TOOLS.FILESYSTEM.MKDIR]: { enabled: false },
      [WORKSPACE_TOOLS.FILESYSTEM.AST_EDIT]: { enabled: false },
      [WORKSPACE_TOOLS.SANDBOX.EXECUTE_COMMAND]: { enabled: false },
      [WORKSPACE_TOOLS.SANDBOX.GET_PROCESS_OUTPUT]: { enabled: false },
      [WORKSPACE_TOOLS.SANDBOX.KILL_PROCESS]: { enabled: false },
    } : {
      [WORKSPACE_TOOLS.FILESYSTEM.WRITE_FILE]: { requireReadBeforeWrite: true },
      [WORKSPACE_TOOLS.FILESYSTEM.EDIT_FILE]: { requireReadBeforeWrite: true },
      [WORKSPACE_TOOLS.FILESYSTEM.AST_EDIT]: { requireReadBeforeWrite: true },
    },
  });
}

const defaultRecord: AgentWorkspace = {
  id: DEFAULT_WORKSPACE_ID,
  name: "Default workspace",
  repositoryPath: process.env.POPAGENT_DEFAULT_REPOSITORY ?? ".",
  createdAt: "",
  updatedAt: "",
};

export const agentWorkspace = createWorkspace(defaultRecord, DEFAULT_WORKSPACE_PATH, false);
export const readOnlyAgentWorkspace = createWorkspace(defaultRecord, DEFAULT_WORKSPACE_PATH, true);

export class AgentWorkspaceRuntime {
  private readonly cache = new Map<string, { updatedAt: string; workspace: Workspace }>();

  constructor(private readonly workspaces: AgentWorkspaceStore = agentWorkspaces) {}

  async resolve(id = DEFAULT_WORKSPACE_ID, readOnly = false): Promise<Workspace> {
    const { workspace: record, path } = await this.workspaces.resolveRepository(id);
    const key = `${record.id}:${readOnly}`;
    const cached = this.cache.get(key);
    if (cached?.updatedAt === record.updatedAt) return cached.workspace;
    const workspace = createWorkspace(record, path, readOnly);
    this.cache.set(key, { updatedAt: record.updatedAt, workspace });
    return workspace;
  }

  async resolveSelfUpdate(id: string, path: string, readOnly: boolean): Promise<Workspace> {
    const containedPath = resolve(path);
    const relativePath = relative(SELF_UPDATE_WORKSPACE_ROOT, containedPath);
    if (!relativePath || relativePath === ".." || relativePath.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(relativePath)) {
      throw new Error("Self-update workspace path is outside the managed root");
    }
    const { workspace: record, path: repositoryPath } = await this.workspaces.resolveRepository(id);
    const dependencyPath = join(repositoryPath, "node_modules");
    const sandboxReadOnlyPaths: string[] = [];
    try {
      await access(dependencyPath);
      sandboxReadOnlyPaths.push(dependencyPath);
    } catch {
      // Dependency-free repositories need no additional read-only mount.
    }
    return createWorkspace(record, containedPath, readOnly, sandboxReadOnlyPaths);
  }
}

export const agentWorkspaceRuntime = new AgentWorkspaceRuntime();