@viloris/vela-plugin-fs
v0.0.1
Published
Host TS capability plugin: app-sandbox + OS-scoped filesystem
Readme
@viloris/vela-plugin-fs
First-party T1 capability plugin: filesystem access for Apps via vela.call("fs.*").
Two path domains share one method family:
| Domain | Paths | Permissions |
|--------|-------|-------------|
| app | App-relative under the sandbox (notes/a.txt) | fs:app-read, fs:app-write |
| os | Absolute OS paths under manifest path scopes | fs:read, fs:write, fs:meta, fs:manage |
| Surface | Value |
|---------|--------|
| Methods | fs.read, fs.write, fs.exists, fs.stat, fs.readDir, fs.mkdir, fs.remove, fs.rename, fs.copy |
| Reserved | fs.watch (permission + contract stub; needs event bus) |
| Systems | HostAPI.sys.fs (injected; mock for tests) |
| Encodings | utf8 (default), base64, bytes (number[] wire) |
Absolute / drive / UNC paths always route to the os domain. App-relative paths reject .. escape and absolute forms.
Host registration
import { createCapabilityHost } from "@viloris/vela-host-core";
import { BuiltinPermissions } from "@viloris/vela-api";
import { registerFsPlugin, createMockFsSys } from "@viloris/vela-plugin-fs";
const mock = createMockFsSys({ files: { "notes/a.txt": "hello" } });
const host = createCapabilityHost({
api: {
platform: "linux",
sys: { fs: mock.facade },
},
capabilities: {
default: {
permissions: [
BuiltinPermissions.FsAppRead,
BuiltinPermissions.FsAppWrite,
// OS domain (optional):
// BuiltinPermissions.FsRead,
// BuiltinPermissions.FsWrite,
// BuiltinPermissions.FsMeta,
// BuiltinPermissions.FsManage,
],
// optional path scopes — resource is the normalized path
scopes: [
{ type: "path", pattern: "notes/**" },
// { type: "path", pattern: "/home/me/Projects/**" },
],
},
},
});
registerFsPlugin(host);App (page)
const { data } = await window.vela.call("fs.read", { path: "notes/a.txt" });
await window.vela.call("fs.write", { path: "notes/b.txt", data: "world" });
// OS path (requires fs:read + path scope):
await window.vela.call("fs.read", { path: "/home/me/Projects/a.txt" });
// thin wrappers:
import {
readAppFile,
writeAppFile,
readFile,
writeFile,
existsPath,
statPath,
readDir,
mkdirPath,
removePath,
renamePath,
copyPath,
} from "@viloris/vela-plugin-fs/client";
await writeAppFile("notes/b.txt", "world");
await mkdirPath("notes/sub", { recursive: true });Real OS filesystem
Inject sys.fs from the privileged Host — never from App TS.
| Backend | Package | Notes |
|---------|---------|--------|
| Node / Bun runtime | @viloris/vela-sys-desktop createDesktopFsSys({ root }) | Default; good for CI |
| Zig kernel | @viloris/vela-sys-desktop createVelaSysFs({ root }) | libs/vela-sys via Bun FFI (ADR 0008) |
import { createDesktopSystems, createVelaSysFs } from "@viloris/vela-sys-desktop";
// Convenience: node backend
const systems = createDesktopSystems({
fs: { root: appDataDir }, // backend: "node" (default)
});
// Or Zig kernel (requires built libvela_sys)
const systemsZig = createDesktopSystems({
fs: { root: appDataDir, backend: "vela-sys" },
});
// Or inject only the facade:
const fs = createVelaSysFs({ root: appDataDir });Build the Zig library once:
cd libs/vela-sys && zig build && zig build test
# produces zig-out/lib/libvela_sys.so (or .dylib / .dll)
# override path: VELA_SYS_LIB=/path/to/libvela_sys.soOS-domain calls re-check path scopes after optional realpath (symlink escape defense).
Pair with dialog:* when the user picks a path outside declared scopes — that is a separate grant path.
Verify
bun test plugins/fs
bun test packages/sys-desktop/src/fs
bun run --filter @viloris/vela-plugin-fs typecheck
cd libs/vela-sys && zig build test