Skip to main content

Overview

The ACP SDK provides methods for agents to read and write files in the client’s file system. These operations are only available if the client advertises the corresponding capabilities during initialization.
File system operations require the client to explicitly enable these capabilities. Always check that the client advertised the capability before attempting to use these methods.

Reading Files

The readTextFile method reads content from a text file:
async readTextFile(
  params: ReadTextFileRequest
): Promise<ReadTextFileResponse>
params.sessionId
string
required
The session ID making the request
params.path
string
required
Absolute path to the file to read
content
string
required
The file contents as a string
Only available if the client advertises the fs.readTextFile capability during initialization.
See protocol docs: Client

Example

class MyAgent implements acp.Agent {
  private connection: acp.AgentSideConnection;
  private clientCapabilities: acp.ClientCapabilities;

  async initialize(params: acp.InitializeRequest): Promise<acp.InitializeResponse> {
    // Store client capabilities
    this.clientCapabilities = params.clientCapabilities;

    return {
      protocolVersion: acp.PROTOCOL_VERSION,
      agentCapabilities: {},
    };
  }

  async readFile(sessionId: string, path: string): Promise<string> {
    // Check if capability is available
    if (!this.clientCapabilities.fs?.readTextFile) {
      throw new Error("Client does not support file reading");
    }

    const response = await this.connection.readTextFile({
      sessionId,
      path,
    });

    return response.content;
  }
}
All paths in the protocol should be absolute paths.

Writing Files

The writeTextFile method writes content to a text file:
async writeTextFile(
  params: WriteTextFileRequest
): Promise<WriteTextFileResponse>
params.sessionId
string
required
The session ID making the request
params.path
string
required
Absolute path to the file to write
params.content
string
required
The content to write to the file
Only available if the client advertises the fs.writeTextFile capability during initialization.
Allows the agent to create or modify files within the client’s environment. See protocol docs: Client

Example

async writeFile(
  sessionId: string,
  path: string,
  content: string
): Promise<void> {
  // Check if capability is available
  if (!this.clientCapabilities.fs?.writeTextFile) {
    throw new Error("Client does not support file writing");
  }

  await this.connection.writeTextFile({
    sessionId,
    path,
    content,
  });
}

Error Handling

Handle errors appropriately when working with file system operations:
import * as acp from "@agentprotocol/acp";

async readFileWithErrorHandling(
  sessionId: string,
  path: string
): Promise<string | null> {
  try {
    const response = await this.connection.readTextFile({
      sessionId,
      path,
    });
    return response.content;
  } catch (error) {
    if (error instanceof acp.RequestError) {
      // Handle specific error codes
      if (error.code === -32002) {
        // Resource not found
        console.error(`File not found: ${path}`);
        return null;
      }
    }
    throw error;
  }
}

async writeFileWithErrorHandling(
  sessionId: string,
  path: string,
  content: string
): Promise<boolean> {
  try {
    await this.connection.writeTextFile({
      sessionId,
      path,
      content,
    });
    return true;
  } catch (error) {
    if (error instanceof acp.RequestError) {
      console.error(`Failed to write file: ${error.message}`);
      return false;
    }
    throw error;
  }
}

Error Codes

-32002
number
Resource not found - the file does not exist
-32603
number
Internal error - an error occurred while reading or writing the file

Security Considerations

File system operations can be sensitive. Always follow these security best practices:

Path Validation

Ensure paths are absolute and within expected boundaries:
import * as path from "node:path";

function validatePath(filePath: string, workingDirectory: string): boolean {
  // Ensure path is absolute
  if (!path.isAbsolute(filePath)) {
    return false;
  }

  // Ensure path is within working directory
  const normalized = path.normalize(filePath);
  const workdir = path.normalize(workingDirectory);
  
  return normalized.startsWith(workdir);
}

Permission Requests

Request user permission before writing sensitive files:
async writeSensitiveFile(
  sessionId: string,
  filePath: string,
  content: string
): Promise<void> {
  // Send tool call for visibility
  await this.connection.sessionUpdate({
    sessionId,
    update: {
      sessionUpdate: "tool_call",
      toolCallId: "write_1",
      title: `Writing to ${path.basename(filePath)}`,
      kind: "edit",
      status: "pending",
      locations: [{ path: filePath }],
      rawInput: { path: filePath, content },
    },
  });

  // Request permission
  const permission = await this.connection.requestPermission({
    sessionId,
    toolCall: {
      toolCallId: "write_1",
      title: `Writing to ${path.basename(filePath)}`,
      kind: "edit",
      status: "pending",
      locations: [{ path: filePath }],
      rawInput: { path: filePath, content },
    },
    options: [
      { kind: "allow_once", name: "Allow", optionId: "allow" },
      { kind: "reject_once", name: "Reject", optionId: "reject" },
    ],
  });

  if (permission.outcome.optionId === "allow") {
    await this.connection.writeTextFile({ sessionId, path: filePath, content });
  }
}

Capability Checking

Always verify capabilities before attempting operations:
class FileSystemHelper {
  constructor(
    private connection: acp.AgentSideConnection,
    private capabilities: acp.ClientCapabilities
  ) {}

  canRead(): boolean {
    return this.capabilities.fs?.readTextFile === true;
  }

  canWrite(): boolean {
    return this.capabilities.fs?.writeTextFile === true;
  }

  async read(sessionId: string, path: string): Promise<string> {
    if (!this.canRead()) {
      throw new Error("Read capability not available");
    }
    const response = await this.connection.readTextFile({ sessionId, path });
    return response.content;
  }

  async write(sessionId: string, path: string, content: string): Promise<void> {
    if (!this.canWrite()) {
      throw new Error("Write capability not available");
    }
    await this.connection.writeTextFile({ sessionId, path, content });
  }
}

Complete Example

Here’s a complete example that demonstrates safe file operations:
import * as acp from "@agentprotocol/acp";
import * as path from "node:path";

class FileOperationAgent implements acp.Agent {
  private connection: acp.AgentSideConnection;
  private capabilities: acp.ClientCapabilities | null = null;

  constructor(connection: acp.AgentSideConnection) {
    this.connection = connection;
  }

  async initialize(params: acp.InitializeRequest): Promise<acp.InitializeResponse> {
    this.capabilities = params.clientCapabilities;
    return {
      protocolVersion: acp.PROTOCOL_VERSION,
      agentCapabilities: {},
    };
  }

  async readProjectFiles(sessionId: string, directory: string): Promise<Map<string, string>> {
    if (!this.capabilities?.fs?.readTextFile) {
      throw new Error("File reading not supported");
    }

    const files = new Map<string, string>();
    const filePaths = await this.listFiles(directory);

    for (const filePath of filePaths) {
      try {
        const response = await this.connection.readTextFile({
          sessionId,
          path: filePath,
        });
        files.set(filePath, response.content);
      } catch (error) {
        console.error(`Failed to read ${filePath}:`, error);
      }
    }

    return files;
  }

  async updateFile(
    sessionId: string,
    filePath: string,
    newContent: string
  ): Promise<boolean> {
    if (!this.capabilities?.fs?.writeTextFile) {
      throw new Error("File writing not supported");
    }

    // Request permission
    const permission = await this.connection.requestPermission({
      sessionId,
      toolCall: {
        toolCallId: crypto.randomUUID(),
        title: `Update ${path.basename(filePath)}`,
        kind: "edit",
        status: "pending",
        locations: [{ path: filePath }],
        rawInput: { path: filePath, content: newContent },
      },
      options: [
        { kind: "allow_once", name: "Allow", optionId: "allow" },
        { kind: "reject_once", name: "Reject", optionId: "reject" },
      ],
    });

    if (permission.outcome.optionId !== "allow") {
      return false;
    }

    await this.connection.writeTextFile({
      sessionId,
      path: filePath,
      content: newContent,
    });

    return true;
  }

  private async listFiles(directory: string): Promise<string[]> {
    // Implementation depends on your file listing strategy
    return [];
  }

  // ... implement other required Agent methods
}

Best Practices

Check Capabilities

Always verify the client supports file operations before attempting them

Use Absolute Paths

All paths in the protocol must be absolute, not relative

Handle Errors

Gracefully handle file not found and permission errors

Request Permission

Request user permission before writing sensitive files

Build docs developers (and LLMs) love