zyris-fs
Serve local files, page text reads, stream binary data, edit safely and understand the filesystem boundary.
zyris-fs provides LocalFileIo, the local-filesystem implementation of file_io v3.
Use it for checkout tools, workstation nodes or services that expose files already on their host.
It uses tokio::fs; it does not require a display, a PTY or a peer transport.
Dependencies
[dependencies]
zyris = { git = "https://github.com/attacca-cc/zyris-protocol", features = ["caps"] }
zyris-fs = { git = "https://github.com/attacca-cc/zyris-protocol" }Add tls-ring or enroll when connecting to Attacca over wss://. Supply a Tokio runtime in the
application. Keep all Zyris dependencies on the same source.
Register the implementation
use zyris::caps::FileIoServer;
use zyris::{Node, NodeKind};
use zyris_fs::LocalFileIo;
fn file_node() -> zyris::Result<Node> {
Node::builder()
.name("files")
.kind(NodeKind::Service)
.capability(FileIoServer(LocalFileIo::rooted("/srv/work")))
.build()
}Construction registers the capability but does not connect. Use Connecting or the complete addon node example.
Tools and arguments
| Tool | Arguments | Result / behavior |
|---|---|---|
stat | path | FileStat: requested path, size, directory flag and optional modification time. |
list | path | Vec<DirEntry>, sorted by name; each entry contains name, size and directory flag. |
read | path, optional offset, optional len | UTF-8 text with byte offsets, actual byte length and a truncated flag. At most 128 KiB per response. |
read_stream | path, optional offset, optional len | A FileStat head followed by byte Chunk items. No 128 KiB total-response ceiling. |
write | path, data: Datum, overwrite: bool | Writes text or inline file/image bytes, creates parent directories, returns the new stat. |
edit | path, old_string, new_string, replace_all: bool | Exact UTF-8 replacement; returns stat and replacement count. |
remove | path, optional recursive | Removes a file or directory. Recursive removal is off when omitted. |
mkdir | path | Creates the directory and missing parents. |
Import the FileIo trait when calling either LocalFileIo or FileIoClient methods.
Page through a text file
use zyris::caps::{FileIo, FileIoClient};
use zyris::Connection;
use std::time::Duration;
async fn read_text(conn: &Connection, path: String) -> zyris::Result<String> {
let files: FileIoClient = conn.wait_capability(Duration::from_secs(5)).await?;
let mut text = String::new();
let mut offset = 0;
loop {
let page = files.read(path.clone(), Some(offset), None).await?;
text.push_str(&page.content);
offset = page.offset + page.len;
if !page.truncated { break; }
if page.len == 0 {
return Err(zyris::Error::internal("file read made no progress"));
}
}
Ok(text)
}len counts source bytes, not Rust characters or the length of the decoded string. Invalid UTF-8
is replaced; a multibyte character split at a page boundary can also be replaced. Use read_stream
for exact bytes rather than concatenating text pages for binary or byte-perfect work.
The helper accumulates the whole result in memory; impose an application limit for untrusted files.
Stream exact bytes
Add futures-util = "0.3" to consume the typed stream:
use futures_util::StreamExt;
use zyris::caps::{FileIo, FileIoClient};
use zyris::Connection;
use std::time::Duration;
async fn count_bytes(conn: &Connection, path: String) -> zyris::Result<u64> {
let files: FileIoClient = conn.wait_capability(Duration::from_secs(5)).await?;
let mut read = files.read_stream(path, None, None).await?;
let mut received = 0;
while let Some(item) = read.items.next().await {
let zyris::Chunk(bytes) = item?;
received += bytes.len() as u64;
}
Ok(received)
}For a file sink, write each chunk instead of collecting it. Save the successfully written byte
offset if you need to restart after disconnect. An agent's unary tool caller may not support
read_stream; use paged read for that caller.
Writes, edits and failures
This example creates a new text file and replaces one known substring. Run it only on a path your
application is authorized to modify; overwrite = false intentionally rejects an existing file.
use std::time::Duration;
use zyris::caps::{FileIo, FileIoClient};
use zyris::{Connection, Datum};
async fn create_note(conn: &Connection) -> zyris::Result<()> {
let files: FileIoClient = conn.wait_capability(Duration::from_secs(5)).await?;
files.write(
"notes/example.txt".into(),
Datum::Text { text: "Status: draft\n".into(), format: None },
false,
).await?;
let edited = files.edit(
"notes/example.txt".into(), "Status: draft".into(), "Status: ready".into(), false,
).await?;
println!("replacements={}", edited.replaced);
Ok(())
}writeaccepts text and inline file/image blobs. Attachment blobs currently fail withinvalid_params; do not mistake protocol attachment support for support in this implementation.overwrite = falsechecks existence, but the check and write are not an atomic create-only operation. It is not a concurrency guarantee against another writer.editrejects an empty search string, invalid UTF-8, no match, or multiple matches whenreplace_allis false. It reads and rewrites the file; there is no compare-and-swap revision check.- Missing paths return
not_found; OS permission errors map toforbidden_scope; other I/O errors normally map tointernal. Hereforbidden_scopecan mean host permissions, not a credential grant.
Security and operational limits
rooted is a path default, not a jail. Absolute paths and .. can leave it, and filesystem
symlinks can point outside it. The implementation neither isolates the OS user nor applies your
product's read/write/delete policy. For an actual boundary, enforce policy consistently across
all methods, including streams, and use appropriate OS isolation. Account for symlinks and races;
a lexical prefix check alone is insufficient.
Use a separate custom capability for a read-only or narrower tool surface, or implement FileIo
over your own storage and policy. See Writing capabilities.