Filesystem contracts and SDK
Two filesystem models exist in ShellCanvas, for two different jobs. A files service gives the desktop browsable, opaque locations. The filesystem SDK gives a native bridge application rooted handles it can hand to an operating-system driver. This page covers both, and where the line between them sits.
Availability
shellcanvas-filesystem-sdk is a Rust crate, published as a provisional 0.1 release. It depends on no filesystem driver, no SSH implementation and nothing from the desktop: it is the neutral middle piece between a provider and a mount.
Interface
Locations in a files service
A files service returns locations the desktop treats as opaque strings. A directory listing carries everything needed to navigate without parsing them:
| Field | Meaning |
|---|---|
path | This folder, as your own token |
name | Display name |
parent | The parent location, or null to disable going up |
home | The user's starting place, or null to hide Home |
roots | Top-level places to show in the sidebar |
entries | Up to 128 per page, each with a name, kind, size, modification time and an entry revision |
Never expect the desktop to split, join or normalise a path — separators, drive letters and case rules are yours. Revisions are how the desktop detects that something changed under it: a stale revision refuses a move, rename or delete rather than acting on a different file.
The filesystem SDK
pub struct MountPath(Vec<String>); // relative components; empty is the granted root
pub enum FsKind { File, Directory, Symlink, Other }
pub enum FsErrorKind {
NotFound, PermissionDenied, AlreadyExists, NotDirectory, IsDirectory, NotEmpty,
InvalidInput, Unsupported, ReadOnly, Offline, TimedOut, Io,
}
#[async_trait]
pub trait MountedFileSystem: Send + Sync {
async fn check_available(&self) -> FsResult<()>;
async fn capabilities(&self) -> FsResult<FsCapabilities>; // writable, atomic_replace, durable_flush
async fn space(&self, path: &MountPath) -> FsResult<FsSpace>;
async fn metadata(&self, path: &MountPath) -> FsResult<FsMetadata>;
async fn open(&self, path: &MountPath, create: FsCreate, options: FsOpenOptions) -> FsResult<Arc<dyn MountedFile>>;
async fn open_directory(&self, path: &MountPath) -> FsResult<Arc<dyn MountedDirectory>>;
async fn set_metadata(&self, path: &MountPath, metadata: FsSetMetadata) -> FsResult<()>;
async fn mkdir(&self, path: &MountPath) -> FsResult<()>;
async fn remove(&self, path: &MountPath, directory: bool) -> FsResult<()>;
async fn rename(&self, from: &MountPath, to: &MountPath, replace: bool) -> FsResult<()>;
}MountPath is deliberately narrow: components only, with no empty names, no . or .., and none of / \ : NUL. A path cannot escape the root it was granted, and there are no drive prefixes to interpret.
The bridge protocol
A bridge application is a separate process, connected by inherited anonymous pipes — never a network socket. Protocol version 2, framed like the adapter protocol with a 4-byte big-endian length, but with its own limits:
| Limit | Value |
|---|---|
| Frame | 1 MiB |
| Read and write chunk | 32 KiB |
| Directory entries per read | 64 |
| Request timeout | 30 seconds |
Requests carry a version, a strictly increasing id and one operation — Open, Read, Write, Mkdir, Rename, ReadDirectory and the rest. Replies carry the same id and either a value or a typed error. A timed-out request retires the transport rather than being replayed: a lost handle cannot be closed safely, and a create may already have taken effect.
Examples
A read-only filesystem over your own data:
use shellcanvas_filesystem_sdk::{async_trait, FsError, FsErrorKind, FsResult, MountPath, MountedFileSystem};
struct Catalog;
#[async_trait]
impl MountedFileSystem for Catalog {
async fn check_available(&self) -> FsResult<()> { Ok(()) }
async fn remove(&self, _path: &MountPath, _directory: bool) -> FsResult<()> {
Err(FsError::new(FsErrorKind::ReadOnly, "This attachment is read-only"))
}
// …the remaining methods
}Map your own errors onto the right kind, because the operating system shows them to users through its own dialogs:
fn map(status: DeviceStatus) -> FsError {
match status {
DeviceStatus::Missing => FsError::new(FsErrorKind::NotFound, "No such item"),
DeviceStatus::Denied => FsError::new(FsErrorKind::PermissionDenied, "Permission denied"),
DeviceStatus::Offline => FsError::new(FsErrorKind::Offline, "Device is offline"),
_ => FsError::new(FsErrorKind::Io, "Device error"),
}
}Errors
| Kind | Use it for |
|---|---|
NotFound, AlreadyExists | The obvious cases; AlreadyExists is what keeps a non-replacing rename honest. |
PermissionDenied, ReadOnly | Refused by the device, or by the attachment's own policy. |
NotDirectory, IsDirectory, NotEmpty | Shape mismatches, including a non-empty directory removal. |
Unsupported | An operation this filesystem does not implement at all. |
Offline | The transport is gone; the bridge retires rather than pretending. |
TimedOut | Bounded waiting expired — the outcome is genuinely unknown. |
InvalidInput, Io | Malformed requests, and everything else. |
Chunk violations are rejected rather than truncated: a read or write above 32 KiB fails with InvalidInput, and a provider that returns more bytes than requested is treated as a fault.
Lifecycle and permissions
Handles are owned by the connection that opened them and closed when it ends; teardown closes what remains under one bounded budget. A bridge process is reviewed and hash-pinned by the desktop before it is ever launched, and runs with the user's operating-system permissions — the SDK is a contract, not a sandbox.
Keep the two models apart in your own code: opaque provider locations are for the desktop and apps, and MountPath is for a driver. Translating between them is the bridge's job, and it is where path-safety mistakes would otherwise happen.
Related guides
- Local drives and mounted filesystems — what a bridge feels like to a user.
- Adapter services — advertising a files service.
- Files API — how apps consume provider locations.
- Adapter process protocol — the other framed protocol, with different limits.