Open an aindrive drive as a read-only folder in Memona
Agents / bot (pull_request) Has been skipped
CI / checks (pull_request) Failing after 1m21s

The plugin reaches a drive through aindrive's remote MCP endpoint with
the drive's access token. Opening a connection asks which tools the
token may call, so it works while the drive's machine sleeps. Listing,
stat and reading map to list_files, stat and read_file; a read is
statted first, and a file whose length changed in between is refused as
a conflict. Reads decode only the window asked for, straight from the
reply pieces, so any 1 MiB slice of a file up to aindrive's 8 MiB limit
fits Memona's per-call fuel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 22:51:35 +09:00
parent 4ef9b217fa
commit 3c2cd123b7
17 changed files with 2536 additions and 2 deletions
+142
View File
@@ -0,0 +1,142 @@
//! Where a drive answers, and how a Memona path names something in it.
use crate::sdk::ErrorCode;
/// The drive's MCP endpoint, from whichever drive address was pasted: the
/// server URL aindrive's MCP panel shows (`…/mcp/d/<id>`) or the drive's own
/// page (`…/d/<id>`, with or without a folder after it). Anything before the
/// drive segments is kept, so a server hosted under a path still works.
pub fn endpoint(input: &str) -> Result<String, ErrorCode> {
let input = input.trim();
let rest = input
.strip_prefix("https://")
.or_else(|| input.strip_prefix("http://"))
.ok_or(ErrorCode::InvalidInput)?;
let origin_end =
input.len() - rest.len() + rest.find(['/', '?', '#']).unwrap_or(rest.len());
let (origin, tail) = input.split_at(origin_end);
if origin_end == input.len() - rest.len() {
return Err(ErrorCode::InvalidInput);
}
let path = tail.split(['?', '#']).next().unwrap_or_default();
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let marker = segments
.iter()
.position(|segment| *segment == "d")
.ok_or(ErrorCode::InvalidInput)?;
let id = segments.get(marker + 1).ok_or(ErrorCode::InvalidInput)?;
if id.is_empty()
|| id.len() > 64
|| !id
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
{
return Err(ErrorCode::InvalidInput);
}
let prefix_end = match marker.checked_sub(1) {
Some(before) if segments[before] == "mcp" => before,
_ => marker,
};
let mut url = origin.to_owned();
for segment in &segments[..prefix_end] {
url.push('/');
url.push_str(segment);
}
url.push_str("/mcp/d/");
url.push_str(id);
Ok(url)
}
/// A drive path is relative to the drive root, and the root itself is empty.
pub fn drive_path(path: &str) -> &str {
path.trim_matches('/')
}
/// A name Memona can show as one path segment. The drive's device may hold
/// names the host refuses, and one of those would fail the whole listing.
pub fn showable(name: &str) -> bool {
!name.is_empty()
&& name != "."
&& name != ".."
&& !name.contains(['/', '\\'])
&& !name.chars().any(char::is_control)
}
#[cfg(test)]
mod tests {
use super::{drive_path, endpoint, showable};
use crate::sdk::ErrorCode;
#[tokio::test]
async fn the_mcp_panel_url_is_used_as_given() {
assert_eq!(
endpoint("https://aindrive.ainetwork.ai/mcp/d/KQHGpl8hFXq8"),
Ok("https://aindrive.ainetwork.ai/mcp/d/KQHGpl8hFXq8".to_owned())
);
assert_eq!(
endpoint(" https://aindrive.ainetwork.ai/mcp/d/KQHGpl8hFXq8/ \n"),
Ok("https://aindrive.ainetwork.ai/mcp/d/KQHGpl8hFXq8".to_owned())
);
}
#[tokio::test]
async fn a_drive_page_address_finds_the_same_endpoint() {
for page in [
"https://aindrive.ainetwork.ai/d/KQHGpl8hFXq8",
"https://aindrive.ainetwork.ai/d/KQHGpl8hFXq8?path=notes",
"https://aindrive.ainetwork.ai/d/KQHGpl8hFXq8/notes/today.md",
"https://aindrive.ainetwork.ai/d/KQHGpl8hFXq8/manage#members",
] {
assert_eq!(
endpoint(page),
Ok("https://aindrive.ainetwork.ai/mcp/d/KQHGpl8hFXq8".to_owned()),
"{page}"
);
}
}
#[tokio::test]
async fn a_self_hosted_server_keeps_its_port_and_base_path() {
assert_eq!(
endpoint("http://localhost:3737/d/a_b-C"),
Ok("http://localhost:3737/mcp/d/a_b-C".to_owned())
);
assert_eq!(
endpoint("https://files.example/aindrive/mcp/d/abc"),
Ok("https://files.example/aindrive/mcp/d/abc".to_owned())
);
}
#[tokio::test]
async fn an_address_without_a_drive_is_refused() {
for input in [
"",
"aindrive.ainetwork.ai/d/abc",
"ftp://aindrive.ainetwork.ai/d/abc",
"https://",
"https://aindrive.ainetwork.ai",
"https://aindrive.ainetwork.ai/d/",
"https://aindrive.ainetwork.ai/d/a%2Fb",
"https://aindrive.ainetwork.ai/drives/abc",
] {
assert_eq!(endpoint(input), Err(ErrorCode::InvalidInput), "{input}");
}
}
#[tokio::test]
async fn memona_paths_become_drive_relative() {
assert_eq!(drive_path("/"), "");
assert_eq!(drive_path("/notes/today.md"), "notes/today.md");
assert_eq!(drive_path("/notes/"), "notes");
}
#[tokio::test]
async fn names_the_host_would_refuse_are_not_shown() {
assert!(showable("작가노트_2026.md"));
assert!(showable(".env"));
assert!(!showable(""));
assert!(!showable(".."));
assert!(!showable("a\\b"));
assert!(!showable("line\nbreak"));
}
}
+200
View File
@@ -0,0 +1,200 @@
//! Base64 decoding cheap enough for Memona's per-call fuel. A read hands over
//! up to 1 MiB in one call, and a general decoder spends more instructions on
//! that than the call is given. Here every two characters cost one table
//! lookup, and the characters are read where they arrived, never copied:
//! the host charges fuel for each byte a plugin copies.
use crate::sdk::ErrorCode;
const ALPHABET: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/// Set in a looked-up word when either character is not base64.
const INVALID: u32 = 1 << 31;
/// Two tables indexed by a pair of characters, read as a little-endian `u16`:
/// the group's first pair and its second pair. Each entry holds that pair's
/// bits already in place in the group's three bytes, read as a little-endian
/// word, so the two halves only need an OR.
static PAIRS: [[u32; 1 << 16]; 2] = pairs();
const fn bytes_of(group: u32) -> u32 {
((group >> 16) & 0xff) | (((group >> 8) & 0xff) << 8) | ((group & 0xff) << 16)
}
const fn pairs() -> [[u32; 1 << 16]; 2] {
let mut tables = [[INVALID; 1 << 16]; 2];
let mut high = 0;
while high < 64 {
let mut low = 0;
while low < 64 {
let index = ALPHABET[high] as usize | (ALPHABET[low] as usize) << 8;
let bits = (high as u32) << 6 | low as u32;
tables[0][index] = bytes_of(bits << 12);
tables[1][index] = bytes_of(bits);
low += 1;
}
high += 1;
}
tables
}
/// One group of four characters: its three bytes in the low 24 bits, and
/// the invalid flag if any character was not base64.
fn group(chars: [u8; 4]) -> u32 {
let chars = u32::from_le_bytes(chars);
PAIRS[0][(chars & 0xffff) as usize] | PAIRS[1][(chars >> 16) as usize]
}
/// Decodes whole groups into `out`, three bytes for every four characters.
/// Sixteen characters go through each step of the main loop, which measured
/// a third cheaper in fuel per character than four at a time.
fn groups(chars: &[u8], out: &mut [u8]) -> u32 {
let wide = chars.chunks_exact(16);
let (wide_out, rest_out) = out.split_at_mut(chars.len() / 16 * 12);
let rest = wide.remainder().chunks_exact(4);
let mut flags = 0;
for (chars, bytes) in wide.zip(wide_out.chunks_exact_mut(12)) {
let (Some(low), Some(high)) =
(chars.first_chunk::<8>(), chars.last_chunk::<8>())
else {
continue;
};
let low = u64::from_le_bytes(*low);
let high = u64::from_le_bytes(*high);
let [a, b, c, d] = [low, low >> 32, high, high >> 32]
.map(|half| group((half as u32).to_le_bytes()));
flags |= a | b | c | d;
let front = u64::from(a) | u64::from(b) << 24 | u64::from(c) << 48;
let back = c >> 16 | d << 8;
bytes[..8].copy_from_slice(&front.to_le_bytes());
bytes[8..].copy_from_slice(&back.to_le_bytes());
}
for (chars, bytes) in rest.zip(rest_out.chunks_exact_mut(3)) {
if let Some(chars) = chars.first_chunk::<4>() {
let word = group(*chars);
flags |= word;
bytes.copy_from_slice(&word.to_le_bytes()[..3]);
}
}
flags
}
/// Decodes standard base64 that arrived in several pieces, read as one run,
/// into `out`. A group split between two pieces is joined on its own. The
/// run holds whole groups and no padding; `last` decodes the final group.
pub fn run<'a>(
pieces: impl Iterator<Item = &'a [u8]>,
out: &mut [u8],
) -> Result<(), ErrorCode> {
let mut written = 0;
let mut carry = [0_u8; 4];
let mut carried = 0;
let mut flags = 0;
for mut piece in pieces {
if carried > 0 {
let taken = piece.len().min(4 - carried);
carry[carried..carried + taken].copy_from_slice(&piece[..taken]);
carried += taken;
piece = &piece[taken..];
if carried < 4 {
continue;
}
let target = out
.get_mut(written..written + 3)
.ok_or(ErrorCode::Internal)?;
let word = group(carry);
flags |= word;
target.copy_from_slice(&word.to_le_bytes()[..3]);
written += 3;
}
let whole = piece.len() / 4 * 4;
let length = whole / 4 * 3;
let target = out
.get_mut(written..written + length)
.ok_or(ErrorCode::Internal)?;
flags |= groups(&piece[..whole], target);
written += length;
carried = piece.len() - whole;
carry[..carried].copy_from_slice(&piece[whole..]);
}
if carried > 0 || written != out.len() || flags & INVALID != 0 {
return Err(ErrorCode::Unavailable);
}
Ok(())
}
/// The bytes of a final group, which may end in one or two `=`.
pub fn last(chars: [u8; 4]) -> Result<Vec<u8>, ErrorCode> {
let padding = chars.iter().rev().take_while(|c| **c == b'=').count();
if padding > 2 {
return Err(ErrorCode::Unavailable);
}
let mut padded = chars;
for char in &mut padded[4 - padding..] {
*char = b'A';
}
let word = group(padded);
if word & INVALID != 0 {
return Err(ErrorCode::Unavailable);
}
Ok(word.to_le_bytes()[..3 - padding].to_vec())
}
#[cfg(test)]
mod tests {
use super::{last, run};
use crate::sdk::ErrorCode;
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
/// Decodes all of `encoded`, split at `cuts`, as the provider does.
fn decode(encoded: &[u8], cuts: &[usize]) -> Result<Vec<u8>, ErrorCode> {
if encoded.is_empty() {
return Ok(Vec::new());
}
let (body, tail) = encoded.split_at(encoded.len() - 4);
let mut pieces = Vec::new();
let mut from = 0;
for cut in cuts.iter().map(|cut| (*cut).min(body.len())) {
pieces.push(&body[from..cut.max(from)]);
from = cut.max(from);
}
pieces.push(&body[from..]);
let mut out = vec![0; body.len() / 4 * 3];
run(pieces.into_iter(), &mut out)?;
let tail: [u8; 4] = tail.try_into().map_err(|_| ErrorCode::Internal)?;
out.extend(last(tail)?);
Ok(out)
}
#[tokio::test]
async fn every_length_and_split_decodes_as_a_general_decoder_would()
-> Result<(), ErrorCode> {
let bytes: Vec<u8> = (0..=255).rev().cycle().take(1000).collect();
for length in 0..1000 {
let encoded = STANDARD.encode(&bytes[..length]);
let encoded = encoded.as_bytes();
for cuts in [&[][..], &[1], &[5, 6, 7], &[17, 33], &[2, 4, 6, 8, 10]] {
assert_eq!(decode(encoded, cuts)?, bytes[..length], "{length}");
}
}
Ok(())
}
#[tokio::test]
async fn anything_but_standard_base64_is_refused() {
for bad in ["QUJ-QUJD", "QUJ_QUJD", "QU\nJQUJD", "QU=DQUJD", "Q===QUJD"] {
assert_eq!(
run([bad.as_bytes()].into_iter(), &mut [0; 6]),
Err(ErrorCode::Unavailable),
"{bad}"
);
}
assert_eq!(last(*b"Q==="), Err(ErrorCode::Unavailable));
assert_eq!(last(*b"QU=D"), Err(ErrorCode::Unavailable));
assert_eq!(
run([&b"QUJ"[..]].into_iter(), &mut [0; 0]),
Err(ErrorCode::Unavailable)
);
}
}
+411
View File
@@ -0,0 +1,411 @@
//! An aindrive filesystem provider for Memona. It speaks to the drive's
//! remote MCP endpoint through the host's HTTP, with the drive's access token.
mod address;
mod decode;
mod mcp;
mod sdk;
#[cfg(test)]
mod session_tests;
#[cfg(test)]
mod test_host;
use sdk::{
Capabilities, ChangePage, Entry, EntryKind, ErrorCode, FileInfo, Guest,
GuestReader, GuestWriter, ListPage, Locator, Reader, Setting, WriteCondition,
Writer,
};
use serde::de::DeserializeOwned;
use serde_json::json;
use std::cell::{Cell, RefCell};
use std::ops::Range;
#[cfg(not(test))]
use sdk::host;
#[cfg(test)]
use test_host as host;
const MAX_CHUNK: u32 = 1024 * 1024;
/// The drive's device reads at most 8 MiB in one call and cuts the rest, so
/// a longer file is refused before any of it is read.
const MAX_READ: u64 = 8 * 1024 * 1024;
/// A whole 8 MiB file in base64, with room for the reply around it.
const MAX_REPLY: usize = 12 * 1024 * 1024;
/// Without these the folder cannot even be browsed.
const REQUIRED_TOOLS: [&str; 3] = ["list_files", "stat", "read_file"];
struct Session {
endpoint: String,
authorization: String,
}
impl Session {
fn from_config(config: &[(String, Setting)]) -> Result<Self, ErrorCode> {
let lookup = |wanted: &str| {
config
.iter()
.find(|(key, _)| key == wanted)
.and_then(|(_, value)| match value {
Setting::Text(text) | Setting::Secret(text) => {
let text = text.trim();
(!text.is_empty()).then(|| text.to_owned())
}
Setting::Toggle(_) => None,
})
};
let endpoint =
address::endpoint(&lookup("url").ok_or(ErrorCode::InvalidInput)?)?;
let token = lookup("token").ok_or(ErrorCode::InvalidInput)?;
Ok(Self {
endpoint,
authorization: format!("Bearer {token}"),
})
}
}
thread_local! {
static SESSION: RefCell<Option<Session>> = const { RefCell::new(None) };
}
struct Aindrive;
fn transport(error: host::HostError) -> ErrorCode {
match error {
host::HostError::Denied => ErrorCode::Denied,
host::HostError::Cancelled => ErrorCode::Cancelled,
host::HostError::InvalidInput => ErrorCode::InvalidInput,
host::HostError::Internal => ErrorCode::Internal,
host::HostError::Unavailable => ErrorCode::Unavailable,
}
}
/// The drive-relative path a locator names; the drive root is empty.
fn route(target: &Locator) -> String {
let Locator::Path(path) = target;
address::drive_path(path).to_owned()
}
fn header(name: &str, value: &str) -> host::Header {
host::Header {
name: name.to_owned(),
value: value.to_owned(),
}
}
/// Sends one JSON-RPC body to the drive and returns the reply in the pieces
/// the host handed over. They are not joined here: Memona's host charges
/// fuel for every byte a plugin copies, and a file's reply joined whole would
/// spend most of a call's budget before any of it was decoded.
fn post(body: &[u8]) -> Result<Vec<Vec<u8>>, ErrorCode> {
let (url, authorization) = SESSION.with_borrow(|session| {
session
.as_ref()
.map(|session| (session.endpoint.clone(), session.authorization.clone()))
.ok_or(ErrorCode::Internal)
})?;
let call = host::http(&host::Request {
method: host::Method::Post,
url,
headers: vec![
header("content-type", "application/json"),
header("accept", "application/json, text/event-stream"),
header("authorization", &authorization),
],
})
.map_err(transport)?;
for piece in body.chunks(MAX_CHUNK as usize) {
call.push(piece).map_err(transport)?;
}
let head = call.respond().map_err(transport)?;
mcp::status(head.status)?;
let mut pieces = Vec::new();
let mut total = 0;
loop {
let piece = call.pull(MAX_CHUNK).map_err(transport)?;
if piece.is_empty() {
return Ok(pieces);
}
total += piece.len();
if total > MAX_REPLY {
return Err(ErrorCode::Unavailable);
}
pieces.push(piece);
}
}
/// A reply small enough to read as one buffer, which it usually already is.
fn joined(mut pieces: Vec<Vec<u8>>) -> Vec<u8> {
match pieces.len() {
1 => pieces.pop().unwrap_or_default(),
_ => pieces.concat(),
}
}
/// Calls one of the drive's tools and reads its structured answer.
fn tool<T: DeserializeOwned>(
name: &str,
arguments: &serde_json::Value,
) -> Result<T, ErrorCode> {
mcp::reply(&joined(post(&mcp::call(name, arguments))?))
}
fn info_of(entry: &mcp::Entry) -> FileInfo {
FileInfo {
kind: if entry.is_dir {
EntryKind::Directory
} else {
EntryKind::File
},
// A folder's size is the device's directory record, not its contents.
size: if entry.is_dir { 0 } else { entry.size },
revision: entry.revision(),
}
}
/// One file read whole from the drive and handed over in pieces, decoded
/// from the reply as each piece is asked for.
struct Download {
reply: Vec<Vec<u8>>,
/// Where the base64 contents sit in the reply, read as one run.
encoded: Range<usize>,
size: u64,
position: Cell<u64>,
revision: Option<String>,
}
impl Download {
/// Where the characters of group `index`, three bytes each, start.
fn chars(&self, index: usize) -> usize {
self.encoded.start + index * 4
}
/// The bytes of one group, the last of which may be padded.
fn group(&self, index: usize) -> Result<Vec<u8>, ErrorCode> {
let mut chars = [0_u8; 4];
let mut filled = 0;
let at = self.chars(index);
for slice in mcp::slices(&self.reply, at..at + 4) {
chars[filled..filled + slice.len()].copy_from_slice(slice);
filled += slice.len();
}
let bytes = decode::last(chars)?;
let final_group = self.encoded.len() / 4 - 1;
if filled != 4 || (index != final_group && bytes.len() != 3) {
return Err(ErrorCode::Unavailable);
}
Ok(bytes)
}
/// Decodes at most `wanted` bytes from byte `start`. A start inside a
/// group of three answers only the rest of that group, a short piece the
/// host reads past, so every later piece starts where a group does and no
/// decoded bytes are shifted into place.
fn decode(&self, start: u64, wanted: usize) -> Result<Vec<u8>, ErrorCode> {
let first = usize::try_from(start / 3).map_err(|_| ErrorCode::Internal)?;
let skip = (start % 3) as usize;
if skip != 0 {
let bytes = self.group(first)?;
let rest = bytes.get(skip..).unwrap_or_default();
return Ok(rest[..rest.len().min(wanted)].to_vec());
}
let final_group = self.encoded.len() / 4 - 1;
let end = first + wanted.div_ceil(3);
let whole = end.min(final_group);
let mut bytes = Vec::with_capacity((end - first) * 3);
bytes.resize((whole - first) * 3, 0);
let chars = self.chars(first)..self.chars(whole);
decode::run(mcp::slices(&self.reply, chars), &mut bytes)?;
if end > final_group {
bytes.extend(self.group(final_group)?);
}
bytes.truncate(wanted);
Ok(bytes)
}
}
impl GuestReader for Download {
fn pull(&self, limit: u32) -> Result<Vec<u8>, ErrorCode> {
let start = self.position.get();
let wanted = u64::from(limit.min(MAX_CHUNK)).min(self.size - start);
if wanted == 0 {
return Ok(Vec::new());
}
let bytes = self.decode(start, wanted as usize)?;
self.position.set(start + bytes.len() as u64);
Ok(bytes)
}
fn revision(&self) -> Option<String> {
self.revision.clone()
}
}
/// Starts reading one file. `read_file` has no offset and no revision, so
/// the file is statted first and read whole. If its length then disagrees
/// with the stat, the file changed in between, and the read is refused as a
/// conflict rather than paired with the wrong revision.
fn download(target: &Locator, offset: u64) -> Result<Download, ErrorCode> {
let path = route(target);
let entry: mcp::Entry = tool("stat", &json!({ "path": path }))?;
if entry.is_dir {
return Err(ErrorCode::InvalidInput);
}
if entry.size > MAX_READ {
return Err(ErrorCode::Unsupported);
}
let (reply, encoded) = if offset >= entry.size {
(Vec::new(), 0..0)
} else {
let length = usize::try_from(entry.size.div_ceil(3) * 4)
.map_err(|_| ErrorCode::Unsupported)?;
let reply = post(&mcp::call(
"read_file",
&json!({ "path": path, "encoding": "base64" }),
))?;
let encoded = mcp::contents(&reply, length)?;
(reply, encoded)
};
Ok(Download {
reply,
encoded,
size: entry.size,
position: Cell::new(offset.min(entry.size)),
revision: entry.revision(),
})
}
/// Writing is not offered yet, so no transfer is ever started.
enum Upload {}
impl GuestWriter for Upload {
fn push(&self, _data: Vec<u8>) -> Result<(), ErrorCode> {
match *self {}
}
fn finish(&self) -> Result<Option<FileInfo>, ErrorCode> {
match *self {}
}
}
impl Guest for Aindrive {
type Reader = Download;
type Writer = Upload;
/// Asking which tools the token may call checks the address and the token
/// without waking the drive's device, so a connection can be saved while
/// that machine sleeps; its files answer once it is back.
fn open(
_provider_id: String,
config: Vec<(String, Setting)>,
) -> Result<Capabilities, ErrorCode> {
SESSION.set(None);
SESSION.set(Some(Session::from_config(&config)?));
let probe = post(mcp::TOOLS).and_then(|pieces| mcp::tools(&joined(pieces)));
let tools = match probe {
Ok(tools) => tools,
Err(error) => {
SESSION.set(None);
return Err(error);
}
};
if !REQUIRED_TOOLS
.iter()
.all(|required| tools.iter().any(|tool| tool == required))
{
SESSION.set(None);
return Err(ErrorCode::Denied);
}
Ok(Capabilities {
write: false,
mkdir: false,
rename: false,
remove: false,
conditional_write: false,
watch: false,
virtual_directories: false,
})
}
fn close() {
SESSION.set(None);
}
/// The stat tool refuses the drive root, which always exists, so the root
/// is listed instead: that still tells whether the device can be reached.
fn stat(target: Locator) -> Result<FileInfo, ErrorCode> {
let path = route(&target);
if path.is_empty() {
let _: mcp::Listing = tool("list_files", &json!({ "path": "" }))?;
return Ok(FileInfo {
kind: EntryKind::Directory,
size: 0,
revision: None,
});
}
let entry: mcp::Entry = tool("stat", &json!({ "path": path }))?;
Ok(info_of(&entry))
}
/// aindrive returns a whole folder at once, so the page is taken from it.
fn list(
target: Locator,
cursor: Option<String>,
limit: u32,
) -> Result<ListPage, ErrorCode> {
let start: usize = match cursor {
Some(value) => value.parse().map_err(|_| ErrorCode::InvalidInput)?,
None => 0,
};
let listing: mcp::Listing =
tool("list_files", &json!({ "path": route(&target) }))?;
let children: Vec<Entry> = listing
.entries
.iter()
.filter(|entry| address::showable(&entry.name))
.map(|entry| Entry {
name: entry.name.clone(),
info: info_of(entry),
})
.collect();
let end = start.saturating_add(limit as usize).min(children.len());
let entries = children.get(start..end).unwrap_or_default().to_vec();
Ok(ListPage {
entries,
cursor: (end < children.len()).then(|| end.to_string()),
})
}
fn open_read(target: Locator, offset: u64) -> Result<Reader, ErrorCode> {
Ok(Reader::new(download(&target, offset)?))
}
fn open_write(
_target: Locator,
_condition: WriteCondition,
_size_hint: Option<u64>,
) -> Result<Writer, ErrorCode> {
Err(ErrorCode::Unsupported)
}
fn mkdir(_target: Locator) -> Result<FileInfo, ErrorCode> {
Err(ErrorCode::Unsupported)
}
fn rename(
_target: Locator,
_destination: String,
) -> Result<FileInfo, ErrorCode> {
Err(ErrorCode::Unsupported)
}
fn remove(_target: Locator) -> Result<(), ErrorCode> {
Err(ErrorCode::Unsupported)
}
fn poll(_cursor: Option<String>) -> Result<ChangePage, ErrorCode> {
Err(ErrorCode::Unsupported)
}
}
#[cfg(not(test))]
sdk::export!(Aindrive with_types_in sdk);
+427
View File
@@ -0,0 +1,427 @@
//! The drive's remote MCP endpoint: one JSON-RPC `tools/call` per request,
//! answered either as JSON or as a single server-sent event.
use crate::sdk::ErrorCode;
use serde::Deserialize;
use serde::de::DeserializeOwned;
use serde_json::value::RawValue;
use std::borrow::Cow;
use std::ops::Range;
/// Lists the tools this token may call. It never reaches the drive's device,
/// so it checks the address and the token even while the device is offline.
pub const TOOLS: &[u8] = br#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#;
/// The body of one tool call. The endpoint keeps no session, so every call
/// stands alone without an `initialize` first.
pub fn call(tool: &str, arguments: &serde_json::Value) -> Vec<u8> {
serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": tool, "arguments": arguments },
})
.to_string()
.into_bytes()
}
/// What an HTTP status means before any body is read. A 404 is the drive
/// itself missing: paths that are not there arrive as tool errors instead.
pub fn status(code: u16) -> Result<(), ErrorCode> {
match code {
200..=299 => Ok(()),
401 | 403 => Err(ErrorCode::Denied),
404 => Err(ErrorCode::NotFound),
_ => Err(ErrorCode::Unavailable),
}
}
/// The message of a reply sent the way the MCP SDK sends one: a single
/// event, the whole message on its one `data:` line. It is borrowed as it
/// stands, without walking a file's worth of bytes to find where it ends.
fn single_event(body: &[u8]) -> Option<&[u8]> {
body
.strip_prefix(b"event: message\n")
.unwrap_or(body)
.strip_prefix(b"data: ")?
.strip_suffix(b"\n\n")
}
/// The JSON-RPC message in any other reply: plain JSON, or the last event of
/// a stream, whose `data:` lines are joined as server-sent events define.
fn message(body: &[u8]) -> Result<Cow<'_, [u8]>, ErrorCode> {
if body.trim_ascii_start().starts_with(b"{") {
return Ok(Cow::Borrowed(body));
}
let mut event: Vec<&[u8]> = Vec::new();
let mut last = Vec::new();
for line in body.split(|byte| *byte == b'\n') {
let line = line.strip_suffix(b"\r").unwrap_or(line);
if line.is_empty() && !event.is_empty() {
last = std::mem::take(&mut event);
}
if let Some(data) = line.strip_prefix(b"data:") {
event.push(data.strip_prefix(b" ").unwrap_or(data));
}
}
if !event.is_empty() {
last = event;
}
match last.as_slice() {
[] => Err(ErrorCode::Unavailable),
[only] => Ok(Cow::Borrowed(only)),
many => Ok(Cow::Owned(many.join(&b'\n'))),
}
}
#[derive(Deserialize)]
#[serde(bound = "T: DeserializeOwned")]
struct Envelope<T> {
result: Option<T>,
error: Option<serde::de::IgnoredAny>,
}
/// A tool result, read in the same pass as its envelope: a file's contents
/// are its largest part, and every extra pass over them costs fuel.
#[derive(Deserialize)]
#[serde(bound = "T: DeserializeOwned")]
struct Outcome<T> {
#[serde(default, rename = "isError")]
is_error: bool,
#[serde(default)]
content: Vec<Text>,
#[serde(rename = "structuredContent")]
structured: Option<T>,
}
#[derive(Deserialize)]
struct Text {
#[serde(default)]
text: String,
}
fn envelope<T: DeserializeOwned>(body: &[u8]) -> Result<T, ErrorCode> {
let envelope: Envelope<T> =
match single_event(body).map(serde_json::from_slice) {
Some(Ok(envelope)) => envelope,
_ => serde_json::from_slice(&message(body)?)
.map_err(|_| ErrorCode::Unavailable)?,
};
if envelope.error.is_some() {
return Err(ErrorCode::InvalidInput);
}
envelope.result.ok_or(ErrorCode::Unavailable)
}
/// A tool's structured answer, or the error it reported. A JSON-RPC error,
/// as opposed to a tool error, means the request itself was malformed.
pub fn reply<T: DeserializeOwned>(body: &[u8]) -> Result<T, ErrorCode> {
let outcome: Outcome<T> = envelope(body)?;
if outcome.is_error {
let text = outcome.content.first().map(|part| part.text.as_str());
return Err(failure(text.unwrap_or_default()));
}
outcome.structured.ok_or(ErrorCode::Unavailable)
}
/// The names of the tools a `tools/list` answer offers.
pub fn tools(body: &[u8]) -> Result<Vec<String>, ErrorCode> {
#[derive(Deserialize)]
struct Listed {
tools: Vec<Tool>,
}
#[derive(Deserialize)]
struct Tool {
name: String,
}
let listed: Listed = envelope(body)?;
Ok(listed.tools.into_iter().map(|tool| tool.name).collect())
}
/// One entry of a listing or a stat, as the drive's device reported it.
#[derive(Deserialize)]
pub struct Entry {
pub name: String,
#[serde(rename = "isDir")]
pub is_dir: bool,
#[serde(default)]
pub size: u64,
/// Kept as the device wrote it: a revision is compared as text, and a
/// float printed again could be spelled differently.
#[serde(default, rename = "mtimeMs")]
mtime: Option<Box<RawValue>>,
}
impl Entry {
/// The revision aindrive compares a conditional write against,
/// `m<mtimeMs>-s<size>`. A folder has none.
pub fn revision(&self) -> Option<String> {
if self.is_dir {
return None;
}
let mtime = self
.mtime
.as_deref()
.map(RawValue::get)
.filter(|text| *text != "null")
.unwrap_or("0");
Some(format!("m{mtime}-s{}", self.size))
}
}
#[derive(Deserialize)]
pub struct Listing {
pub entries: Vec<Entry>,
}
/// A whole file, as `read_file` returns it. The device stops at 8 MiB and
/// says so with `truncated` rather than failing.
#[derive(Deserialize)]
pub struct Contents {
pub content: String,
pub encoding: String,
#[serde(default)]
pub truncated: bool,
}
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}
/// The parts of `pieces`, read as one run, that `range` covers.
pub fn slices(
pieces: &[Vec<u8>],
range: Range<usize>,
) -> impl Iterator<Item = &[u8]> {
let mut offset = 0;
pieces.iter().filter_map(move |piece| {
let from = offset;
offset += piece.len();
let start = range.start.max(from);
let end = range.end.min(offset);
(start < end).then(|| &piece[start - from..end - from])
})
}
/// Where the base64 contents sit in a `read_file` reply, read as one run,
/// found without reading through them. They are nearly the whole reply, and
/// parsing them as a JSON string would spend a call's fuel on bytes base64
/// never escapes. The stat says how long they must be; the rest of the reply,
/// with them left out, is still read whole to check it was a complete base64
/// read.
pub fn contents(
pieces: &[Vec<u8>],
length: usize,
) -> Result<Range<usize>, ErrorCode> {
const SECTION: &[u8] = br#""structuredContent":"#;
const FIELD: &[u8] = br#""content":""#;
let head = pieces.first().map(Vec::as_slice).unwrap_or_default();
let start =
find(&head[..head.len().min(64 * 1024)], SECTION).and_then(|at| {
let from = at + SECTION.len();
let near = &head[from..head.len().min(from + 1024)];
find(near, FIELD).map(|field| from + field + FIELD.len())
});
let Some(start) = start else {
// A failed read carries no contents and is small enough to join.
let _: Contents = reply(&pieces.concat())?;
return Err(ErrorCode::Unavailable);
};
// Contents of another length are another version of the file than the
// one statted, so the revision in hand does not describe them.
let end = start.saturating_add(length);
let total = pieces.iter().map(Vec::len).sum();
if slices(pieces, end..total)
.next()
.and_then(|rest| rest.first())
!= Some(&b'"')
{
return Err(ErrorCode::Conflict);
}
let mut rest = head[..start].to_vec();
for slice in slices(pieces, end..total) {
rest.extend_from_slice(slice);
}
let contents: Contents = reply(&rest)?;
if !contents.content.is_empty()
|| contents.encoding != "base64"
|| contents.truncated
{
return Err(ErrorCode::Unavailable);
}
Ok(start..end)
}
/// What a tool error means to a filesystem caller. aindrive writes them as
/// `[code] message`, and an `internal` one carries the device's own error,
/// so the device's errno names are read too. Messages are never shown on:
/// they can hold the absolute path on the owner's machine.
fn failure(text: &str) -> ErrorCode {
let (code, message) = text
.strip_prefix('[')
.and_then(|rest| rest.split_once("] "))
.unwrap_or(("internal", text));
match code {
"not_found" => ErrorCode::NotFound,
"forbidden" => ErrorCode::Denied,
"invalid_params" if message.starts_with("conflict") => ErrorCode::Conflict,
"invalid_params" => ErrorCode::InvalidInput,
_ => device(message),
}
}
fn device(message: &str) -> ErrorCode {
const KNOWN: &[(&str, ErrorCode)] = &[
("ENOENT", ErrorCode::NotFound),
("EACCES", ErrorCode::Denied),
("EPERM", ErrorCode::Denied),
("reserved path", ErrorCode::Denied),
("EEXIST", ErrorCode::Conflict),
("ENOTEMPTY", ErrorCode::Conflict),
("EISDIR", ErrorCode::InvalidInput),
("is a directory", ErrorCode::InvalidInput),
("ENOTDIR", ErrorCode::InvalidInput),
("ENAMETOOLONG", ErrorCode::InvalidInput),
("path too long", ErrorCode::InvalidInput),
];
KNOWN
.iter()
.find(|(needle, _)| message.contains(needle))
.map_or(ErrorCode::Unavailable, |(_, code)| *code)
}
#[cfg(test)]
mod tests {
use super::{Entry, Listing, call, reply, status, tools};
use crate::sdk::ErrorCode;
use serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq)]
struct Answer {
ok: bool,
}
fn tool_error(text: &str) -> Vec<u8> {
let message = serde_json::json!({
"result": {
"_meta": {},
"content": [{ "type": "text", "text": text }],
"isError": true,
},
"jsonrpc": "2.0",
"id": 1,
});
format!("event: message\ndata: {message}\n\n").into_bytes()
}
#[tokio::test]
async fn a_call_names_the_tool_and_its_arguments() -> Result<(), ErrorCode> {
let body = call("list_files", &serde_json::json!({ "path": "notes" }));
let sent: serde_json::Value =
serde_json::from_slice(&body).map_err(|_| ErrorCode::Internal)?;
assert_eq!(sent["method"], "tools/call");
assert_eq!(sent["params"]["name"], "list_files");
assert_eq!(sent["params"]["arguments"]["path"], "notes");
Ok(())
}
#[tokio::test]
async fn an_event_stream_and_plain_json_read_the_same()
-> Result<(), ErrorCode> {
let json = br#"{"result":{"content":[{"type":"text","text":"ok"}],"structuredContent":{"ok":true}},"jsonrpc":"2.0","id":1}"#;
let event = [b"event: message\r\ndata: ", &json[..], b"\r\n\r\n"].concat();
assert_eq!(reply::<Answer>(json)?, Answer { ok: true });
assert_eq!(reply::<Answer>(&event)?, Answer { ok: true });
let split = b"event: message\ndata: {\"result\":\ndata: {\"structuredContent\":{\"ok\":true}}}\n\n";
assert_eq!(reply::<Answer>(split)?, Answer { ok: true });
Ok(())
}
#[tokio::test]
async fn tool_errors_keep_missing_denied_and_conflicting_apart() {
let cases = [
("[not_found] no entry at notes/a.md", ErrorCode::NotFound),
(
"[forbidden] forbidden (need editor, have viewer)",
ErrorCode::Denied,
),
("[forbidden] payment required for paid", ErrorCode::Denied),
(
"[invalid_params] conflict: the file changed since that revision",
ErrorCode::Conflict,
),
("[invalid_params] invalid path: ..", ErrorCode::InvalidInput),
("[internal] agent offline", ErrorCode::Unavailable),
(
"[internal] ENOENT: no such file or directory, scandir '/Users/me/x'",
ErrorCode::NotFound,
),
(
"[internal] ENOTDIR: not a directory",
ErrorCode::InvalidInput,
),
("[internal] is a directory", ErrorCode::InvalidInput),
("something unexpected", ErrorCode::Unavailable),
];
for (text, expected) in cases {
assert_eq!(reply::<Answer>(&tool_error(text)), Err(expected), "{text}");
}
}
#[tokio::test]
async fn a_rejected_request_is_not_mistaken_for_a_reply() {
let rejected =
br#"{"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"bad"}}"#;
assert_eq!(reply::<Answer>(rejected), Err(ErrorCode::InvalidInput));
assert_eq!(reply::<Answer>(b"<html>"), Err(ErrorCode::Unavailable));
assert_eq!(reply::<Answer>(b""), Err(ErrorCode::Unavailable));
}
#[tokio::test]
async fn the_tool_list_names_what_the_token_may_call() -> Result<(), ErrorCode>
{
let body = br#"event: message
data: {"result":{"tools":[{"name":"list_files","inputSchema":{}},{"name":"read_file"}]},"jsonrpc":"2.0","id":1}
"#;
assert_eq!(tools(body)?, vec!["list_files", "read_file"]);
Ok(())
}
#[tokio::test]
async fn a_revision_spells_the_modification_time_as_the_device_did()
-> Result<(), ErrorCode> {
let listing: Listing = serde_json::from_str(
r#"{"entries":[
{"name":"a.md","isDir":false,"size":1837,"mtimeMs":1791726579102.7002},
{"name":"b.md","isDir":false,"size":5,"mtimeMs":1791726579100},
{"name":"c.md","isDir":false,"size":0},
{"name":"notes","isDir":true,"size":96,"mtimeMs":1791726603429.636}
]}"#,
)
.map_err(|_| ErrorCode::Internal)?;
let revisions: Vec<_> =
listing.entries.iter().map(Entry::revision).collect();
assert_eq!(
revisions,
[
Some("m1791726579102.7002-s1837".to_owned()),
Some("m1791726579100-s5".to_owned()),
Some("m0-s0".to_owned()),
None,
]
);
Ok(())
}
#[tokio::test]
async fn statuses_keep_the_token_apart_from_the_drive() {
assert_eq!(status(200), Ok(()));
assert_eq!(status(401), Err(ErrorCode::Denied));
assert_eq!(status(403), Err(ErrorCode::Denied));
assert_eq!(status(404), Err(ErrorCode::NotFound));
assert_eq!(status(502), Err(ErrorCode::Unavailable));
}
}
+15
View File
@@ -0,0 +1,15 @@
//! Rust guest bindings for the versioned Memona filesystem component contract,
//! generated from the copy of the canonical WIT under `wit/`.
wit_bindgen::generate!({
path: "wit",
world: "filesystem",
pub_export_macro: true,
});
pub use exports::memona::filesystem::provider::{
Capabilities, ChangePage, Entry, EntryKind, ErrorCode, FileInfo, Guest,
GuestReader, GuestWriter, ListPage, Locator, Reader, WriteCondition, Writer,
};
pub use memona::filesystem::host;
pub use memona::filesystem::host::Setting;
+287
View File
@@ -0,0 +1,287 @@
//! The provider against an in-memory aindrive. Live QA separately runs the
//! compiled component through Memona's host against the real service.
use super::{Aindrive, SESSION, download, test_host};
use crate::sdk::{EntryKind, ErrorCode, Guest, GuestReader, Locator, Setting};
use std::collections::BTreeMap;
const ENDPOINT: &str = "https://aindrive.ainetwork.ai/mcp/d/KQHGpl8hFXq8";
fn config() -> Vec<(String, Setting)> {
vec![
(
"url".to_owned(),
Setting::Text("https://aindrive.ainetwork.ai/d/KQHGpl8hFXq8".to_owned()),
),
(
"token".to_owned(),
Setting::Secret(" aind_pat_secret\n".to_owned()),
),
]
}
fn at(path: &str) -> Locator {
Locator::Path(path.to_owned())
}
fn sample() -> test_host::Drive {
let mut files = BTreeMap::new();
for (path, bytes, mtime) in [
("PAGE.md", &b"# Studio"[..], "1791731214739.939"),
("notes/작가노트_2026.md", &b"notes"[..], "1791726579100"),
("notes/odd\\name.md", &b"x"[..], "1"),
("artworks.tsv", &b"title\tyear"[..], "1791726579103.648"),
] {
files.insert(path.to_owned(), test_host::file(bytes, mtime));
}
test_host::Drive {
files,
folders: vec!["outputs".to_owned()],
..test_host::Drive::default()
}
}
#[tokio::test]
async fn opening_checks_the_token_against_the_drive_endpoint()
-> Result<(), ErrorCode> {
let drive = test_host::install(sample());
let capabilities = Aindrive::open("aindrive".to_owned(), config())?;
assert!(!capabilities.write && !capabilities.remove && !capabilities.watch);
let drive = drive.borrow();
let [seen] = drive.seen.as_slice() else {
return Err(ErrorCode::Internal);
};
assert_eq!(seen.url, ENDPOINT);
assert_eq!(seen.method, "tools/list");
assert_eq!(
seen.authorization.as_deref(),
Some("Bearer aind_pat_secret")
);
Ok(())
}
#[tokio::test]
async fn a_refused_token_or_a_missing_drive_leaves_no_connection() {
for (status, expected) in
[(401, ErrorCode::Denied), (404, ErrorCode::NotFound)]
{
let _drive = test_host::install(test_host::Drive {
status: Some(status),
..sample()
});
assert_eq!(
Aindrive::open("aindrive".to_owned(), config()).err(),
Some(expected)
);
assert!(SESSION.with_borrow(Option::is_none));
}
}
#[tokio::test]
async fn a_token_that_cannot_read_files_is_refused() {
let _drive = test_host::install(test_host::Drive {
tools: Some(vec!["search"]),
..sample()
});
assert_eq!(
Aindrive::open("aindrive".to_owned(), config()).err(),
Some(ErrorCode::Denied)
);
assert!(SESSION.with_borrow(Option::is_none));
}
#[tokio::test]
async fn a_config_without_a_token_or_drive_is_invalid() {
let _drive = test_host::install(sample());
let only_url = config().into_iter().take(1).collect();
let only_token = config().into_iter().skip(1).collect();
for config in [only_url, only_token] {
assert_eq!(
Aindrive::open("aindrive".to_owned(), config).err(),
Some(ErrorCode::InvalidInput)
);
}
}
#[tokio::test]
async fn listing_pages_through_a_folder_and_hides_unshowable_names()
-> Result<(), ErrorCode> {
let _drive = test_host::install(sample());
Aindrive::open("aindrive".to_owned(), config())?;
let first = Aindrive::list(at("/"), None, 2)?;
let names: Vec<_> = first.entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, ["PAGE.md", "artworks.tsv"]);
assert_eq!(
first.entries[0].info.revision.as_deref(),
Some("m1791731214739.939-s8")
);
let rest = Aindrive::list(at("/"), first.cursor, 2)?;
let names: Vec<_> = rest.entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, ["notes", "outputs"]);
assert!(rest.cursor.is_none());
assert!(matches!(rest.entries[0].info.kind, EntryKind::Directory));
assert_eq!(rest.entries[0].info.size, 0);
assert!(rest.entries[0].info.revision.is_none());
let notes = Aindrive::list(at("/notes/"), None, 10)?;
let names: Vec<_> = notes.entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, ["작가노트_2026.md"]);
Ok(())
}
#[tokio::test]
async fn listing_a_file_or_a_missing_folder_says_which() -> Result<(), ErrorCode>
{
let _drive = test_host::install(sample());
Aindrive::open("aindrive".to_owned(), config())?;
assert_eq!(
Aindrive::list(at("/PAGE.md"), None, 10).err(),
Some(ErrorCode::InvalidInput)
);
assert_eq!(
Aindrive::list(at("/gone"), None, 10).err(),
Some(ErrorCode::NotFound)
);
assert_eq!(
Aindrive::list(at("/"), Some("x".to_owned()), 10).err(),
Some(ErrorCode::InvalidInput)
);
Ok(())
}
#[tokio::test]
async fn stat_reports_the_root_files_and_folders() -> Result<(), ErrorCode> {
let _drive = test_host::install(sample());
Aindrive::open("aindrive".to_owned(), config())?;
let root = Aindrive::stat(at("/"))?;
assert!(matches!(root.kind, EntryKind::Directory));
let page = Aindrive::stat(at("/PAGE.md"))?;
assert!(matches!(page.kind, EntryKind::File));
assert_eq!(page.size, 8);
assert_eq!(page.revision.as_deref(), Some("m1791731214739.939-s8"));
assert!(matches!(
Aindrive::stat(at("/outputs"))?.kind,
EntryKind::Directory
));
assert_eq!(
Aindrive::stat(at("/missing.md")).err(),
Some(ErrorCode::NotFound)
);
Ok(())
}
/// Reads all of a file from `offset`, as the host does, in pulls of
/// `limit` bytes that may come back shorter.
fn read_from(
path: &str,
offset: u64,
limit: u32,
) -> Result<(Vec<u8>, Option<String>), ErrorCode> {
let reader = download(&at(path), offset)?;
let mut read = Vec::new();
loop {
let piece = reader.pull(limit)?;
assert!(piece.len() <= limit as usize);
if piece.is_empty() {
return Ok((read, reader.revision()));
}
read.extend_from_slice(&piece);
}
}
#[tokio::test]
async fn reads_start_anywhere_and_never_exceed_the_limit()
-> Result<(), ErrorCode> {
// Longer than one of the test host's 4 KiB reply pieces, so groups of
// base64 characters are split between pieces.
let bytes: Vec<u8> = (0..=255).cycle().take(20_000).collect();
let mut drive = sample();
drive
.files
.insert("data.bin".to_owned(), test_host::file(&bytes, "5"));
let _drive = test_host::install(drive);
Aindrive::open("aindrive".to_owned(), config())?;
for offset in [0_u64, 1, 2, 3, 3070, 3071, 10_000, 19_998, 19_999] {
for limit in [1, 7, 4096, 1024 * 1024] {
let (read, revision) = read_from("/data.bin", offset, limit)?;
assert_eq!(read, bytes[offset as usize..], "{offset} by {limit}");
assert_eq!(revision.as_deref(), Some("m5-s20000"));
}
}
assert!(read_from("/data.bin", 20_000, 10)?.0.is_empty());
assert!(read_from("/data.bin", 50_000, 10)?.0.is_empty());
Ok(())
}
#[tokio::test]
async fn files_shorter_than_a_group_and_empty_files_read_whole()
-> Result<(), ErrorCode> {
let mut drive = sample();
for (name, bytes) in [("one", &b"a"[..]), ("two", b"ab"), ("empty", b"")] {
drive
.files
.insert(name.to_owned(), test_host::file(bytes, "1"));
}
let _drive = test_host::install(drive);
Aindrive::open("aindrive".to_owned(), config())?;
assert_eq!(read_from("/one", 0, 10)?.0, b"a");
assert_eq!(read_from("/two", 1, 10)?.0, b"b");
assert_eq!(read_from("/empty", 0, 10)?.0, b"");
Ok(())
}
#[tokio::test]
async fn a_file_changed_between_stat_and_read_is_a_conflict()
-> Result<(), ErrorCode> {
let _drive = test_host::install(test_host::Drive {
change_on_read: Some(test_host::file(b"# Studio, longer now", "2")),
..sample()
});
Aindrive::open("aindrive".to_owned(), config())?;
assert_eq!(
download(&at("/PAGE.md"), 0).err(),
Some(ErrorCode::Conflict)
);
Ok(())
}
#[tokio::test]
async fn folders_missing_files_and_oversized_files_are_not_read()
-> Result<(), ErrorCode> {
let mut drive = sample();
drive.files.insert(
"large.bin".to_owned(),
test_host::file(&vec![0; 8 * 1024 * 1024 + 1], "9"),
);
let drive = test_host::install(drive);
Aindrive::open("aindrive".to_owned(), config())?;
assert_eq!(
download(&at("/notes"), 0).err(),
Some(ErrorCode::InvalidInput)
);
assert_eq!(
download(&at("/missing.md"), 0).err(),
Some(ErrorCode::NotFound)
);
drive.borrow_mut().seen.clear();
assert_eq!(
download(&at("/large.bin"), 0).err(),
Some(ErrorCode::Unsupported)
);
let tools: Vec<_> = drive
.borrow()
.seen
.iter()
.map(|seen| seen.tool.clone())
.collect();
assert_eq!(tools, [Some("stat".to_owned())]);
Ok(())
}
#[tokio::test]
async fn nothing_is_requested_before_a_connection_opens() {
let drive = test_host::install(sample());
Aindrive::close();
assert_eq!(Aindrive::stat(at("/")).err(), Some(ErrorCode::Internal));
assert!(drive.borrow().seen.is_empty());
}
+258
View File
@@ -0,0 +1,258 @@
//! An in-memory aindrive behind the generated synchronous `Guest` API. It
//! answers tool calls the way the drive's MCP endpoint does, as one
//! server-sent event; live QA covers Memona's real HTTP host and the service.
pub use crate::sdk::host::{Header, HostError, Method, Request, Response};
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use serde_json::{Value, json};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
pub struct File {
pub bytes: Vec<u8>,
/// Written into replies verbatim, the way the device's float prints.
pub mtime: &'static str,
}
#[derive(Default)]
pub struct Drive {
/// Drive-relative paths. A folder is any prefix of a file, or listed here.
pub files: BTreeMap<String, File>,
pub folders: Vec<String>,
/// The tools `tools/list` offers; every tool by default.
pub tools: Option<Vec<&'static str>>,
/// The status every request answers with, when not 200.
pub status: Option<u16>,
/// Replaces a file between a stat and the read that follows it.
pub change_on_read: Option<File>,
pub seen: Vec<Seen>,
}
pub struct Seen {
pub url: String,
pub authorization: Option<String>,
pub method: String,
pub tool: Option<String>,
}
thread_local! {
static ACTIVE: RefCell<Option<Rc<RefCell<Drive>>>> =
const { RefCell::new(None) };
}
pub fn install(drive: Drive) -> Rc<RefCell<Drive>> {
let drive = Rc::new(RefCell::new(drive));
ACTIVE.set(Some(drive.clone()));
drive
}
pub fn file(bytes: &[u8], mtime: &'static str) -> File {
File {
bytes: bytes.to_vec(),
mtime,
}
}
pub fn http(request: &Request) -> Result<Exchange, HostError> {
if !matches!(request.method, Method::Post) {
return Err(HostError::InvalidInput);
}
let drive = ACTIVE
.with_borrow(Clone::clone)
.ok_or(HostError::Internal)?;
Ok(Exchange {
drive,
request: request.clone(),
body: RefCell::new(Vec::new()),
reply: RefCell::new(None),
})
}
pub struct Exchange {
drive: Rc<RefCell<Drive>>,
request: Request,
body: RefCell<Vec<u8>>,
reply: RefCell<Option<Vec<u8>>>,
}
impl Exchange {
pub fn push(&self, data: &[u8]) -> Result<(), HostError> {
if data.len() > 1024 * 1024 || self.reply.borrow().is_some() {
return Err(HostError::InvalidInput);
}
self.body.borrow_mut().extend_from_slice(data);
Ok(())
}
pub fn respond(&self) -> Result<Response, HostError> {
let call: Value = serde_json::from_slice(&self.body.borrow())
.map_err(|_| HostError::InvalidInput)?;
let mut drive = self.drive.borrow_mut();
let authorization = self
.request
.headers
.iter()
.find(|header| header.name == "authorization")
.map(|header| header.value.clone());
drive.seen.push(Seen {
url: self.request.url.clone(),
authorization,
method: call["method"].as_str().unwrap_or_default().to_owned(),
tool: call["params"]["name"].as_str().map(str::to_owned),
});
let (status, reply) = match drive.status {
Some(status) => (status, b"{\"error\":\"refused\"}".to_vec()),
None => (200, event(&drive.answer(&call))),
};
*self.reply.borrow_mut() = Some(reply);
Ok(Response {
status,
headers: Vec::new(),
})
}
/// Hands the reply over in pieces smaller than asked for, so a caller
/// that stops at the first short piece loses the rest.
pub fn pull(&self, limit: u32) -> Result<Vec<u8>, HostError> {
let mut reply = self.reply.borrow_mut();
let reply = reply.as_mut().ok_or(HostError::InvalidInput)?;
let count = reply.len().min(limit as usize).min(4096);
Ok(reply.drain(..count).collect())
}
}
fn event(message: &Value) -> Vec<u8> {
format!("event: message\ndata: {message}\n\n").into_bytes()
}
fn success(structured: Value) -> Value {
json!({
"result": {
"_meta": { "ai.aindrive/a2ui": [] },
"content": [{ "type": "text", "text": "ok" }],
"structuredContent": structured,
},
"jsonrpc": "2.0",
"id": 1,
})
}
fn failure(text: &str) -> Value {
json!({
"result": {
"_meta": {},
"content": [{ "type": "text", "text": text }],
"isError": true,
},
"jsonrpc": "2.0",
"id": 1,
})
}
impl Drive {
fn answer(&mut self, call: &Value) -> Value {
if call["method"] == "tools/list" {
let tools = self.tools.clone().unwrap_or_else(|| {
vec![
"list_files",
"read_file",
"write_file",
"delete_path",
"stat",
]
});
let tools: Vec<Value> =
tools.iter().map(|name| json!({ "name": name })).collect();
return json!({ "result": { "tools": tools }, "jsonrpc": "2.0", "id": 1 });
}
let arguments = &call["params"]["arguments"];
let path = arguments["path"].as_str().unwrap_or_default().to_owned();
match call["params"]["name"].as_str().unwrap_or_default() {
"list_files" => match self.children(&path) {
Some(entries) => success(json!({ "entries": entries })),
None if self.files.contains_key(&path) => {
failure("[internal] ENOTDIR: not a directory, scandir '/Users/me'")
}
None => failure("[internal] ENOENT: no such file or directory"),
},
"stat" if path.is_empty() => failure("[invalid_params] path required"),
"stat" => match self.entry(&path) {
Some(entry) => success(entry),
None => failure(&format!("[not_found] no entry at {path}")),
},
"read_file" => {
if let Some(changed) = self.change_on_read.take() {
self.files.insert(path.clone(), changed);
}
match self.files.get(&path) {
Some(file) => success(json!({
"method": "read",
"content": STANDARD.encode(&file.bytes),
"encoding": "base64",
"truncated": false,
})),
None if self.is_folder(&path) => failure("[internal] is a directory"),
None => failure("[internal] ENOENT: no such file or directory"),
}
}
_ => failure("[internal] unknown tool"),
}
}
fn is_folder(&self, path: &str) -> bool {
path.is_empty()
|| self.folders.iter().any(|folder| folder == path)
|| self
.files
.keys()
.any(|file| file.starts_with(&format!("{path}/")))
}
fn entry(&self, path: &str) -> Option<Value> {
let name = path.rsplit('/').next().unwrap_or(path);
if let Some(file) = self.files.get(path) {
let entry = format!(
r#"{{"name":{},"path":{},"isDir":false,"size":{},"mtimeMs":{}}}"#,
json!(name),
json!(path),
file.bytes.len(),
file.mtime,
);
return serde_json::from_str(&entry).ok();
}
self.is_folder(path).then(|| {
json!({ "name": name, "path": path, "isDir": true, "size": 96,
"mtimeMs": 1_791_726_544_422.142_f64 })
})
}
fn children(&self, path: &str) -> Option<Vec<Value>> {
if !self.is_folder(path) {
return None;
}
let prefix = if path.is_empty() {
String::new()
} else {
format!("{path}/")
};
let mut names: Vec<&str> = self
.files
.keys()
.chain(self.folders.iter())
.filter_map(|item| item.strip_prefix(&prefix))
.filter_map(|rest| rest.split('/').next())
.filter(|name| !name.is_empty())
.collect();
names.dedup();
names.sort_unstable();
names.dedup();
Some(
names
.into_iter()
.filter_map(|name| self.entry(&format!("{prefix}{name}")))
.collect(),
)
}
}