Direct volume paths are absolute volume-internal paths rooted at /. Relative paths are rejected, .. cannot escape the volume root, and there is no machine cwd or user context for direct volume file operations.

Read and write

from nullspace import Volume

volume = Volume.from_name("team-data", create_if_missing=True)
volume.files.make_dir("/datasets")
volume.files.write("/datasets/report.txt", "ready\n")
print(volume.files.read("/datasets/report.txt"))
print(volume.files.read("/datasets/report.txt", format="bytes"))
printf "ready\n" | nullspace volume write team-data /datasets/report.txt
nullspace volume read team-data /datasets/report.txt
nullspace volume read team-data /datasets/report.txt --encoding base64
curl -X POST "${NULLSPACE_API_URL}/v1/volumes/vol_123/files/mkdir" \
  -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"path": "/datasets"}'

curl -X POST "${NULLSPACE_API_URL}/v1/volumes/vol_123/files/write" \
  -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"path": "/datasets/report.txt", "content": "ready\n"}'

curl -X POST "${NULLSPACE_API_URL}/v1/volumes/vol_123/files/read" \
  -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"path": "/datasets/report.txt"}'

List and inspect

for item in volume.files.list("/datasets"):
    print(item.path, item.size)

print(volume.files.exists("/datasets/report.txt"))
print(volume.files.info("/datasets/report.txt").size)
nullspace volume ls-files team-data /datasets
nullspace volume exists team-data /datasets/report.txt
nullspace volume info team-data /datasets/report.txt
curl -X GET "${NULLSPACE_API_URL}/v1/volumes/vol_123/files?path=/datasets" \
  -H "Authorization: Bearer ${NULLSPACE_API_KEY}"

curl -X POST "${NULLSPACE_API_URL}/v1/volumes/vol_123/files/exists" \
  -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"path": "/datasets/report.txt"}'

curl -X POST "${NULLSPACE_API_URL}/v1/volumes/vol_123/files/info" \
  -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"path": "/datasets/report.txt"}'

Search and replace

matches = volume.files.find_files("/datasets", "ready")
paths = volume.files.search_files("/datasets", "*.txt")
result = volume.files.replace_in_files(paths, "ready", "done", dry_run=True)
print(result.dry_run, result.replacements, result.files_modified)
VolumeReplaceResult also includes the matched file list, so dry runs can show the planned changes before writing.

Move and remove

volume.files.rename("/datasets/report.txt", "/datasets/report-old.txt")
volume.files.set_permissions("/datasets/report-old.txt", "0644")
volume.files.remove("/datasets/report-old.txt")
nullspace volume mv team-data /datasets/report.txt /datasets/report-old.txt
nullspace volume chmod team-data /datasets/report-old.txt 0644
nullspace volume rm team-data /datasets/report-old.txt
curl -X POST "${NULLSPACE_API_URL}/v1/volumes/vol_123/files/rename" \
  -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"old_path": "/datasets/report.txt", "new_path": "/datasets/report-old.txt"}'

curl -X POST "${NULLSPACE_API_URL}/v1/volumes/vol_123/files/permissions" \
  -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"path": "/datasets/report-old.txt", "permissions": "0644"}'

curl -X POST "${NULLSPACE_API_URL}/v1/volumes/vol_123/files/remove" \
  -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"path": "/datasets/report-old.txt"}'

Watch

def on_event(event):
    print(event.type, event.path)

with volume.files.watch_dir("/", on_event=on_event, recursive=True):
    volume.files.write("/datasets/changed.txt", "changed\n")
nullspace volume watch team-data / --recursive --timeout 30
Volume watch is not a plain REST call: it streams change events over the volume data plane rather than a single request/response endpoint, so there is no curl equivalent. Direct volume watches are best-effort notifications from one selected data-plane host. In multi-host deployments, mutations performed through another host may require a resync; clients should treat resync_required as a prompt to list or stat the watched tree again. volume.files.replace_in_files() returns a VolumeReplaceResult. Machine filesystem replacement returns a plain integer count. Direct watches are opened against a concrete volume ID under the hood. If you look up a volume by name, the SDK resolves it first and then watches that ID.

Batch writes

from nullspace import BatchWriteError

try:
    volume.files.write_files([
        ("/datasets/a.txt", "a\n"),
        ("/datasets/b.txt", "b\n"),
    ])
except BatchWriteError as exc:
    print(exc.successes, exc.failures)
nullspace volume write-files team-data '[["/datasets/a.txt","a\n"],["/datasets/b.txt","b\n"]]'
There is no dedicated volume batch-write REST route; over HTTP, issue one POST /v1/volumes/{id}/files/write per file.

Async

The async client mirrors the sync surface above. Volume content search and replace (shown earlier) are SDK-only — the CLI does not expose them.
from nullspace import AsyncVolume

volume = await AsyncVolume.from_name("team-data", create_if_missing=True)
await volume.files.write("/hello.txt", "persistent\n")
print(await volume.files.read("/hello.txt"))