Skip to content

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:

FieldMeaning
pathThis folder, as your own token
nameDisplay name
parentThe parent location, or null to disable going up
homeThe user's starting place, or null to hide Home
rootsTop-level places to show in the sidebar
entriesUp 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

rust
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:

LimitValue
Frame1 MiB
Read and write chunk32 KiB
Directory entries per read64
Request timeout30 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:

rust
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:

rust
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

KindUse it for
NotFound, AlreadyExistsThe obvious cases; AlreadyExists is what keeps a non-replacing rename honest.
PermissionDenied, ReadOnlyRefused by the device, or by the attachment's own policy.
NotDirectory, IsDirectory, NotEmptyShape mismatches, including a non-empty directory removal.
UnsupportedAn operation this filesystem does not implement at all.
OfflineThe transport is gone; the bridge retires rather than pretending.
TimedOutBounded waiting expired — the outcome is genuinely unknown.
InvalidInput, IoMalformed 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.

ShellCanvas documentation