201 lines
11 KiB
Plaintext
201 lines
11 KiB
Plaintext
package memona:filesystem@0.1.0;
|
|
|
|
/// Host services scoped to the current connection. No ambient filesystem,
|
|
/// environment, sockets, credentials, or network permissions are inherited.
|
|
interface host {
|
|
/// Stable failures without backend messages or credential-bearing URLs.
|
|
enum host-error { denied, unavailable, invalid-input, cancelled, internal }
|
|
/// Severity of a diagnostic event. The host may discard message contents.
|
|
enum log-level { debug, info, warn, error }
|
|
|
|
/// Every method the host will send. A verb outside this list cannot be
|
|
/// spelled, so no plugin can smuggle one past the transport rules.
|
|
enum method {
|
|
get, head, post, put, patch, delete,
|
|
propfind, proppatch, mkcol, copy, move, lock, unlock, report,
|
|
}
|
|
/// An HTTP header. Host-controlled transport headers cannot be overridden.
|
|
record header { name: string, value: string }
|
|
/// Request head. At most 64 headers of 8 KiB each; the body is streamed.
|
|
record request { method: method, url: string, headers: list<header> }
|
|
/// Response head. The body is read from the exchange that produced it.
|
|
record response { status: u16, headers: list<header> }
|
|
/// One HTTP request and its response, streamed in bounded chunks so a
|
|
/// transfer is never held whole in memory. A body of any size crosses in
|
|
/// pieces, which is what multipart uploads of at least 5 MiB require.
|
|
/// Dropping the handle abandons the request.
|
|
resource exchange {
|
|
/// Appends at most 1,048,576 bytes to the request body. Every push
|
|
/// must land before `respond`.
|
|
push: func(data: list<u8>) -> result<_, host-error>;
|
|
/// Ends the request body and waits for the response head. Calling it
|
|
/// twice is invalid-input.
|
|
respond: func() -> result<response, host-error>;
|
|
/// Reads at most 1,048,576 bytes of the response body, after
|
|
/// `respond`. An empty result means the body is complete.
|
|
pull: func(limit: u32) -> result<list<u8>, host-error>;
|
|
}
|
|
/// Begins a request to a host both declared by the package and granted on
|
|
/// the connection. Each redirect and DNS result is checked, and DNS is
|
|
/// pinned. Private/LAN addresses additionally require the private-network
|
|
/// grant. Cross-origin redirects remove request headers; HTTPS cannot
|
|
/// downgrade. The host bounds total bytes per exchange, not buffer size.
|
|
http: func(request: request) -> result<exchange, host-error>;
|
|
|
|
/// One open TCP connection. Dropping the handle closes the socket, and the
|
|
/// host closes every socket when the connection is disposed.
|
|
resource tcp {
|
|
/// Reads at most `limit` bytes, between 1 and 1,048,576. An empty
|
|
/// result means the peer closed its side, not that data is pending.
|
|
read: func(limit: u32) -> result<list<u8>, host-error>;
|
|
/// Writes at most 1,048,576 bytes and returns how many were accepted.
|
|
write: func(data: list<u8>) -> result<u32, host-error>;
|
|
}
|
|
/// Opens a socket to a host and port both declared by the package and
|
|
/// granted on the connection. The host resolves and pins DNS, applies the
|
|
/// same private-address rule as `http`, and starts TLS when asked. Ports
|
|
/// negotiated at runtime, such as an FTP data channel, still need a grant.
|
|
connect: func(host: string, port: u16, tls: bool) -> result<tcp, host-error>;
|
|
|
|
/// One datagram socket connected to a single granted peer.
|
|
resource udp {
|
|
/// Sends one datagram of at most 65,507 bytes.
|
|
send: func(data: list<u8>) -> result<u32, host-error>;
|
|
/// Receives one datagram of at most `limit` bytes. An empty result
|
|
/// means none arrived inside the host's wait, not that the peer left.
|
|
receive: func(limit: u32) -> result<list<u8>, host-error>;
|
|
}
|
|
/// Connects a datagram socket under the same declaration and grant rules
|
|
/// as `connect`, for the few filesystem protocols that still need one.
|
|
bind: func(host: string, port: u16) -> result<udp, host-error>;
|
|
|
|
/// A configuration value, typed as the package's manifest declared it.
|
|
/// A secret is never logged by the host and never leaves the connection.
|
|
variant setting { text(string), secret(string), toggle(bool) }
|
|
/// Reads configuration or a session-only credential for this connection.
|
|
config: func(key: string) -> option<setting>;
|
|
|
|
/// Emits a diagnostic event. Never include secrets or private document data.
|
|
log: func(level: log-level, message: string);
|
|
}
|
|
|
|
/// Filesystem operations for one connection. Paths are absolute within its
|
|
/// root, starting with `/`; they are never local OS paths or resource URIs.
|
|
/// Dot segments, backslashes, percent escapes, colons, and controls are invalid.
|
|
/// Every invocation is bounded by host fuel, memory, time, and cancellation.
|
|
interface provider {
|
|
use host.{setting};
|
|
|
|
/// Stable operation failures. A mutation interrupted after dispatch can
|
|
/// report outcome-unknown: inspect remote state before deciding to retry.
|
|
enum error-code {
|
|
not-found, denied, unsupported, conflict, unavailable, invalid-input,
|
|
cancelled, outcome-unknown, internal
|
|
}
|
|
/// Anything that is neither a file nor a directory, such as a link, is
|
|
/// reported as other rather than flattened into one of the two.
|
|
enum entry-kind { file, directory, other }
|
|
|
|
/// An opaque version token, at most 4096 UTF-8 bytes.
|
|
type revision = string;
|
|
|
|
/// How one resource is named: a path within the connection root.
|
|
variant locator { path(string) }
|
|
|
|
/// Optional operations. False capabilities must return unsupported.
|
|
/// Conditional-write promises atomic create-only and revision comparisons.
|
|
/// Virtual-directories says a directory is only ever a prefix of something
|
|
/// stored, as in a flat object store: the host then never offers to create
|
|
/// an empty one, warns that removing the last child removes the folder,
|
|
/// and does not call a vanished folder an error. Such a provider may still
|
|
/// accept mkdir by writing a marker, and must answer unsupported when
|
|
/// asked to rename a directory.
|
|
record capabilities {
|
|
write: bool, mkdir: bool, rename: bool, remove: bool,
|
|
conditional-write: bool, watch: bool,
|
|
virtual-directories: bool,
|
|
}
|
|
/// Byte length and an opaque revision, each if known.
|
|
record file-info {
|
|
kind: entry-kind,
|
|
size: u64,
|
|
revision: option<revision>,
|
|
}
|
|
/// One child name, without slashes, dot segments, or control characters.
|
|
record entry { name: string, info: file-info }
|
|
/// At most the requested number of entries. A cursor (maximum 4096 bytes)
|
|
/// identifies another page; absence means enumeration is complete.
|
|
record list-page { entries: list<entry>, cursor: option<string> }
|
|
/// Atomic preconditions: absent file, exact opaque revision, or unconditional
|
|
/// replacement. Providers must never simulate comparisons non-atomically.
|
|
variant write-condition { create-only, if-match(revision), overwrite }
|
|
/// Changes since a previous polling cursor.
|
|
enum change-kind { created, modified, removed }
|
|
/// A changed resource path relative to the connection root.
|
|
record change { path: string, kind: change-kind }
|
|
/// At most 256 changes and a cursor of at most 4096 UTF-8 bytes.
|
|
record change-page { changes: list<change>, cursor: option<string> }
|
|
|
|
/// One transfer being read. The handle survives between host calls, so a
|
|
/// file of any size is read as a series of separately bounded calls.
|
|
/// Dropping it releases whatever the provider held open.
|
|
resource reader {
|
|
/// Reads onward from the last position, at most `limit` bytes, with
|
|
/// `limit` between 1 and 1,048,576. An empty result means end of file.
|
|
pull: func(limit: u32) -> result<list<u8>, error-code>;
|
|
/// The revision this transfer is reading, once the provider knows it.
|
|
/// A provider that sees the revision change mid-transfer must fail the
|
|
/// next pull with conflict rather than mix two versions.
|
|
revision: func() -> option<revision>;
|
|
}
|
|
/// One transfer being written. Dropping the handle before it finishes
|
|
/// abandons the transfer, and the provider must leave no partial file.
|
|
resource writer {
|
|
/// Appends at most 1,048,576 bytes. Oversized pushes are rejected,
|
|
/// never truncated.
|
|
push: func(data: list<u8>) -> result<_, error-code>;
|
|
/// Commits the transfer and evaluates the write condition now, against
|
|
/// the state the caller last saw. A none result means the commit needs
|
|
/// another call: keep calling until it answers some, and treat an
|
|
/// interruption in between as outcome-unknown.
|
|
finish: func() -> result<option<file-info>, error-code>;
|
|
}
|
|
|
|
/// Opens exactly one host-owned connection per component instance. The host
|
|
/// passes only this connection's configuration and in-memory credentials.
|
|
open: func(provider-id: string, config: list<tuple<string, setting>>) -> result<capabilities, error-code>;
|
|
/// Best-effort cleanup before disposal. The host may dispose immediately
|
|
/// after a trap, cancellation, permission change, or process shutdown.
|
|
close: func();
|
|
/// Reads metadata without changing the resource.
|
|
stat: func(target: locator) -> result<file-info, error-code>;
|
|
/// Enumerates 1-256 direct children per request, without recursive traversal.
|
|
%list: func(target: locator, cursor: option<string>, limit: u32) -> result<list-page, error-code>;
|
|
/// Begins reading at a byte offset. Streaming, rather than one capped read,
|
|
/// is what lets a provider serve a file larger than a single call allows.
|
|
open-read: func(target: locator, offset: u64) -> result<reader, error-code>;
|
|
/// Begins replacing one complete file if the write capability is present.
|
|
/// The size hint, when the caller knows it, lets a provider choose between
|
|
/// a single request and a multipart upload before the first byte.
|
|
open-write: func(target: locator, condition: write-condition, size-hint: option<u64>) -> result<writer, error-code>;
|
|
/// Creates one directory if the mkdir capability is present.
|
|
mkdir: func(target: locator) -> result<file-info, error-code>;
|
|
/// Renames within this connection if the rename capability is present.
|
|
rename: func(target: locator, destination: string) -> result<file-info, error-code>;
|
|
/// Removes one file or empty directory if the remove capability is present.
|
|
remove: func(target: locator) -> result<_, error-code>;
|
|
/// Polls for a bounded change page if the watch capability is present. No
|
|
/// background callback or persistent subscription is installed by the host.
|
|
poll: func(cursor: option<string>) -> result<change-page, error-code>;
|
|
}
|
|
|
|
/// Memona filesystem extension ABI 0.1.0, distributed as a WebAssembly component.
|
|
/// Rust guests can target wasm32-wasip2. Selected WASI 0.2 CLI, clocks, random,
|
|
/// IO and filesystem interfaces are allowed for the standard library, with
|
|
/// empty arguments/environment, empty preopens, and discarded stdio. WASI
|
|
/// sockets and HTTP imports are not allowed; networking uses the host interface.
|
|
world filesystem {
|
|
import host;
|
|
export provider;
|
|
}
|