Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
import io.agentscope.harness.agent.filesystem.model.ReadResult;
import io.agentscope.harness.agent.filesystem.model.WriteResult;
import io.agentscope.harness.agent.filesystem.util.FilesystemUtils;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;

Expand All @@ -41,8 +41,8 @@
* <p>This class provides default implementations for all {@link AbstractFilesystem} methods by
* delegating
* to shell commands via {@link #execute}. File listing, grep, and glob use standard Unix
* commands. Read uses server-side commands for paginated access. Write delegates content
* transfer to {@link #uploadFiles}. Edit uses server-side commands for string replacement.
* commands. Read uses server-side commands for paginated access. Write and edit delegate content
* transfer to {@link #uploadFiles} and {@link #downloadFiles}.
*
* <p>Subclasses must implement:
* <ul>
Expand Down Expand Up @@ -163,13 +163,14 @@ public ReadResult read(RuntimeContext runtimeContext, String filePath, int offse
@Override
public WriteResult write(RuntimeContext runtimeContext, String filePath, String content) {
String escapedPath = FilesystemUtils.shellQuote(filePath);
String escapedParent = FilesystemUtils.shellQuote(parentDirectory(filePath));
String checkCmd =
"if [ -e "
+ escapedPath
+ " ]; then echo 'EXISTS'; exit 1; fi; "
+ "mkdir -p \"$(dirname "
+ escapedPath
+ ")\" 2>&1";
+ "mkdir -p "
+ escapedParent
+ " 2>&1";

ExecuteResponse checkResult = execute(runtimeContext, checkCmd, null);
if (checkResult.exitCode() != null && checkResult.exitCode() != 0) {
Expand All @@ -180,7 +181,12 @@ public WriteResult write(RuntimeContext runtimeContext, String filePath, String
+ " because it already exists. Read and then make an"
+ " edit, or write to a new path.");
}
return WriteResult.fail("Failed to write file '" + filePath + "'");
String detail = checkResult.output() != null ? checkResult.output().strip() : "";
return WriteResult.fail(
"Failed to write file '"
+ filePath
+ "'"
+ (detail.isEmpty() ? "" : ": " + detail));
}

List<FileUploadResponse> responses =
Expand All @@ -207,84 +213,43 @@ public EditResult edit(
String oldString,
String newString,
boolean replaceAll) {
String payload =
"{\"path\":\""
+ jsonEscape(filePath)
+ "\","
+ "\"old\":\""
+ jsonEscape(oldString)
+ "\","
+ "\"new\":\""
+ jsonEscape(newString)
+ "\","
+ "\"replace_all\":"
+ replaceAll
+ "}";
String payloadB64 =
Base64.getEncoder()
.encodeToString(payload.getBytes(java.nio.charset.StandardCharsets.UTF_8));

String cmd =
"python3 -c \"import sys, os, base64, json\\n"
+ "payload ="
+ " json.loads(base64.b64decode(sys.stdin.read().strip()).decode('utf-8'))\\n"
+ "path, old, new = payload['path'], payload['old'], payload['new']\\n"
+ "replace_all = payload.get('replace_all', False)\\n"
+ "if not os.path.isfile(path):\\n"
+ " print(json.dumps({'error': 'file_not_found'}))\\n"
+ " sys.exit(0)\\n"
+ "with open(path, 'rb') as f: text = f.read().decode('utf-8')\\n"
+ "count = text.count(old)\\n"
+ "if count == 0:\\n"
+ " print(json.dumps({'error': 'string_not_found'}))\\n"
+ " sys.exit(0)\\n"
+ "if count > 1 and not replace_all:\\n"
+ " print(json.dumps({'error': 'multiple_occurrences', 'count': count}))\\n"
+ " sys.exit(0)\\n"
+ "result = text.replace(old, new) if replace_all else text.replace(old, new,"
+ " 1)\\n"
+ "with open(path, 'wb') as f: f.write(result.encode('utf-8'))\\n"
+ "print(json.dumps({'count': count}))\\n"
+ "\" 2>&1 <<'__EDIT_EOF__'\n"
+ payloadB64
+ "\n__EDIT_EOF__\n";

ExecuteResponse result = execute(runtimeContext, cmd, null);
String output = result.output() != null ? result.output().strip() : "";
List<FileDownloadResponse> downloads = downloadFiles(runtimeContext, List.of(filePath));
if (downloads.isEmpty()) {
return EditResult.fail(
"Error editing file '" + filePath + "': download returned no response");
}

if (output.contains("\"error\"")) {
if (output.contains("file_not_found")) {
FileDownloadResponse download = downloads.get(0);
if (!download.isSuccess() || download.content() == null) {
if ("file_not_found".equals(download.error())) {
return EditResult.fail("Error: File '" + filePath + "' not found");
}
if (output.contains("string_not_found")) {
return EditResult.fail("Error: String not found in file: '" + oldString + "'");
}
if (output.contains("multiple_occurrences")) {
return EditResult.fail(
"Error: String '"
+ oldString
+ "' appears multiple times. Use replaceAll=true to replace all"
+ " occurrences.");
}
return EditResult.fail("Error editing file '" + filePath + "': " + output);
return EditResult.fail("Error editing file '" + filePath + "': " + download.error());
}

if (output.contains("\"count\"")) {
try {
int countIdx = output.indexOf("\"count\":") + 8;
int endIdx = output.indexOf('}', countIdx);
int count = Integer.parseInt(output.substring(countIdx, endIdx).trim());
return EditResult.ok(filePath, count);
} catch (NumberFormatException e) {
return EditResult.ok(filePath, 1);
}
String content =
normalizeLineEndings(new String(download.content(), StandardCharsets.UTF_8));
String normalizedOld = normalizeLineEndings(oldString);
String normalizedNew = normalizeLineEndings(newString);
Object[] replacement =
FilesystemUtils.performStringReplacement(
content, normalizedOld, normalizedNew, replaceAll);
if (replacement.length == 1) {
return EditResult.fail((String) replacement[0]);
}

return EditResult.fail(
"Error editing file '"
+ filePath
+ "': unexpected server response: "
+ output.substring(0, Math.min(200, output.length())));
String updated = (String) replacement[0];
int occurrences = (int) replacement[1];
List<FileUploadResponse> uploads =
uploadFiles(
runtimeContext,
List.of(Map.entry(filePath, updated.getBytes(StandardCharsets.UTF_8))));
if (uploads.isEmpty() || !uploads.get(0).isSuccess()) {
String error =
uploads.isEmpty() ? "upload returned no response" : uploads.get(0).error();
return EditResult.fail("Error editing file '" + filePath + "': " + error);
}
return EditResult.ok(filePath, occurrences);
}

@Override
Expand Down Expand Up @@ -439,14 +404,15 @@ private static long parseEpochSeconds(String s) {
return epochSec * 1000;
}

private static String jsonEscape(String s) {
if (s == null) {
return "";
protected static String parentDirectory(String path) {
int slash = path.lastIndexOf('/');
if (slash < 0) {
return ".";
}
return s.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
return slash == 0 ? "/" : path.substring(0, slash);
}

private static String normalizeLineEndings(String value) {
return value.replace("\r\n", "\n").replace("\r", "\n");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,11 @@ public List<FileUploadResponse> uploadFiles(
try {
String base64Content = Base64.getEncoder().encodeToString(content);
String escapedPath = shellSingleQuote(path);
String escapedParent = shellSingleQuote(parentDirectory(path));
String cmd =
"mkdir -p $(dirname "
+ escapedPath
+ ") && "
"mkdir -p "
+ escapedParent
+ " && "
+ "printf '%s' '"
+ base64Content
+ "' | base64 -d > "
Expand Down
Loading
Loading