Skip to main content
Mole transferring files

File Transfer Endpoints

Move files to and from any agent in your mesh. Upload configs, download logs, or transfer entire directories.

Quick examples:

# Upload a file
curl -X POST http://localhost:8080/agents/abc123/file/upload \
-F "file=@./config.yaml" \
-F "path=/tmp/config.yaml"

# Download a file
curl -X POST http://localhost:8080/agents/abc123/file/download \
-d '{"path":"/var/log/app.log"}' -o app.log

POST /agents/{agent-id}/file/upload

Upload file or directory to remote agent.

Content-Type: multipart/form-data

Form Fields:

  • file: File to upload (required)
  • path: Remote destination path (required)
  • password: Authentication password (optional)
  • directory: "true" if uploading directory tar (optional)
  • rate_limit: Max transfer speed in bytes/second (optional)
  • offset: Resume from byte offset (optional)
  • original_size: Expected file size for resume validation (optional)

Response:

{
"success": true,
"bytes_written": 1024,
"remote_path": "/tmp/myfile.txt"
}

Example:

curl -X POST http://localhost:8080/agents/abc123/file/upload   -F "file=@./data.bin"   -F "path=/tmp/data.bin"   -F "password=secret"

POST /agents/{agent-id}/file/download

Download file or directory from remote agent.

Request:

{
"password": "your-password",
"path": "/tmp/myfile.txt",
"rate_limit": 1048576,
"offset": 0,
"original_size": 0
}
FieldTypeRequiredDescription
passwordstringNoAuthentication password
pathstringYesRemote file path to download
rate_limitint64NoMax transfer speed in bytes/second (0 = unlimited)
offsetint64NoResume from byte offset
original_sizeint64NoExpected file size for resume validation

Response: Binary file data

Headers:

  • Content-Type: application/octet-stream (file) or application/gzip (directory)
  • Content-Disposition: Filename
  • X-File-Mode: File permissions (octal, e.g., "0644")

Example:

curl -X POST http://localhost:8080/agents/abc123/file/download   -H "Content-Type: application/json"   -d '{"password":"secret","path":"/tmp/data.bin"}'   -o data.bin

POST /agents/{agent-id}/file/browse

Browse the filesystem on a remote agent. Supports directory listing, file stat, and discovering browsable root paths. Uses the same allowed_paths and password_hash configuration as file transfer.

Action: list

List directory contents with pagination.

Request:

{
"action": "list",
"path": "/tmp",
"password": "secret",
"offset": 0,
"limit": 100
}
FieldTypeRequiredDescription
actionstringNo"list" (default), "stat", "roots", "chmod", or "delete"
pathstringYesDirectory path to list
passwordstringNoAuthentication password
offsetintNoPagination offset (default 0)
limitintNoMax entries to return (default 100, max 200)

Response:

{
"path": "/tmp",
"entries": [
{ "name": "subdir", "size": 4096, "mode": "0755", "mod_time": "2026-02-17T08:00:00Z", "is_dir": true },
{ "name": "file.txt", "size": 1024, "mode": "0644", "mod_time": "2026-02-18T10:30:00Z", "is_dir": false },
{ "name": "link", "size": 12, "mode": "0777", "mod_time": "2026-02-16T12:00:00Z", "is_dir": false, "is_symlink": true, "link_target": "/etc/hosts" }
],
"total": 3,
"truncated": false
}

Entries are sorted with directories first, then files, alphabetically by name within each group. Symlinks include is_symlink and link_target fields, with size and is_dir resolved from the symlink target. For broken symlinks, size and mode reflect the symlink itself rather than the missing target.

Action: stat

Get info about a single path.

Request:

{ "action": "stat", "path": "/tmp/file.txt", "password": "secret" }

Response:

{
"path": "/tmp/file.txt",
"entry": { "name": "file.txt", "size": 1024, "mode": "0644", "mod_time": "2026-02-18T10:30:00Z", "is_dir": false }
}

Action: chmod

Change file permissions on a remote agent.

Request:

{
"action": "chmod",
"path": "/tmp/script.sh",
"mode": "0755",
"password": "secret"
}
FieldTypeRequiredDescription
actionstringYes"chmod"
pathstringYesFile or directory path
passwordstringNoAuthentication password
modestringYesOctal permission string (e.g. "0755", "0644")

Mode must be a valid octal value up to 0777. Returns the updated file entry (same format as stat).

Response:

{
"path": "/tmp/script.sh",
"entry": { "name": "script.sh", "size": 1024, "mode": "0755", "mod_time": "2026-02-18T10:30:00Z", "is_dir": false }
}

Action: delete

Delete a file or directory on a remote agent.

Request:

{
"action": "delete",
"path": "/tmp/old-config.yaml",
"password": "secret"
}
FieldTypeRequiredDescription
actionstringYes"delete"
pathstringYesFile or directory path to delete
passwordstringNoAuthentication password
recursiveboolNoRequired for non-empty directories (default false)

Files, symlinks, and empty directories are deleted directly. Non-empty directories require recursive: true -- without it, the request is rejected. This mirrors standard rm / rm -r behavior.

The response returns the entry info captured before deletion, confirming what was removed.

Response:

{
"path": "/tmp/old-config.yaml",
"entry": { "name": "old-config.yaml", "size": 1024, "mode": "0644", "mod_time": "2026-02-18T10:30:00Z", "is_dir": false }
}

Delete a non-empty directory:

{
"action": "delete",
"path": "/tmp/old-logs",
"recursive": true
}

Action: roots

Discover browsable root paths from the allowed_paths configuration.

Request:

{ "action": "roots", "password": "secret" }

Response:

{ "roots": ["/tmp", "/data"] }

When allowed_paths: ["*"], the response is { "roots": ["/"], "wildcard": true }. Glob patterns like /data/** are normalized to their base directory /data.

Example:

# List directory
curl -X POST http://localhost:8080/agents/abc123/file/browse \
-H "Content-Type: application/json" \
-d '{"action":"list","path":"/tmp"}'

# Stat a file
curl -X POST http://localhost:8080/agents/abc123/file/browse \
-H "Content-Type: application/json" \
-d '{"action":"stat","path":"/tmp/config.yaml"}'

# Get browsable roots
curl -X POST http://localhost:8080/agents/abc123/file/browse \
-H "Content-Type: application/json" \
-d '{"action":"roots"}'

# Change file permissions
curl -X POST http://localhost:8080/agents/abc123/file/browse \
-H "Content-Type: application/json" \
-d '{"action":"chmod","path":"/tmp/script.sh","mode":"0755"}'

# Delete a file
curl -X POST http://localhost:8080/agents/abc123/file/browse \
-H "Content-Type: application/json" \
-d '{"action":"delete","path":"/tmp/old-config.yaml"}'

# Delete a non-empty directory
curl -X POST http://localhost:8080/agents/abc123/file/browse \
-H "Content-Type: application/json" \
-d '{"action":"delete","path":"/tmp/old-logs","recursive":true}'

Errors

HTTP StatusBodyCause
405Method Not AllowedRequest used GET instead of POST
400{"error": "..."}Authentication failure, invalid path, unknown action
503{"error": "file browsing not configured"}File transfer is disabled on the target agent

Implementation Notes

  • Files are streamed in 16KB chunks
  • No inherent size limits
  • Directories are automatically tar/gzip compressed
  • File permissions are preserved

Rate Limiting

When rate_limit is set, the sending agent throttles the transfer to the specified bytes per second. The rate limiter uses a token bucket algorithm with a 16KB burst size.

Resume Transfers

To resume an interrupted transfer:

  1. Set offset to the number of bytes already received
  2. Set original_size to the expected total file size

The remote agent validates that the file size matches original_size. If the file was modified (size changed), the transfer fails with error code ErrResumeFailed (19).

Resume is not supported for directory transfers (tar archives).

Security

  • Requires file_transfer.enabled: true
  • Optional password authentication via file_transfer.password_hash
  • Path restrictions via file_transfer.allowed_paths

See Also