* Extension tasks run through the host: spacedrive_op, task.begin/end with deadline, manifest grants parsed strictly
JobContext::run executes a #[task] with the retries and timeout the attribute
declares. The host records each attempt in the job log and enforces the
deadline: once it passes, every host operation the task makes fails with
timeout, which is the only way a single-threaded guest can be stopped.
spacedrive_op is the one import every data-returning operation goes through;
the answer is written into memory from the guest's own wasm_alloc. The old
spacedrive_call, its fixed-offset writer and the Wire-method permission model
go; ManifestPermissions is the grant shape photos declares (read_records,
read/write_sidecars, write_tags, write_custom_fields, dispatch_jobs,
use_models) and refuses unknown fields.
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
* Entropy and clock host functions
wasm32-unknown-unknown has neither. spacedrive_random fills guest memory
from the host's RNG and the SDK installs it as getrandom's custom backend,
so Uuid::new_v4 and rand work in a guest built with
--cfg getrandom_backend="custom". spacedrive_now_ms is the host's clock
behind spacedrive_sdk::clock::now(), which replaces Utc::now() in guests.
Neither needs a manifest grant: random ids and the time are not library
data.
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
* Records replace entries in the SDK, read from the library's source stores
The entry world is gone, so the SDK's query and read surface is defined
over source-store records: Record (uuid, source, name, kind, extension,
source-relative path, size, mtime, content uuid), get_record, read_record
and a RecordQuery narrowed by source, directory and extension. The host
answers from the stores of the job's library and reads bytes through the
source's resolved root; the manifest's read_records grant (its glob names
extensions) is checked on every call. A tag filter is refused rather than
ignored until tags have a host side.
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
* Sidecars for extensions, keyed by content uuid and an extension-declared kind
SidecarKind opens: Extension { extension_id, kind } beside the core kinds,
living under extensions/<id>/<kind> in the library's content-addressed
sidecar tree (sd-sidecar-path owns the directory), so an extension's kinds
never collide with the core's or another's. The SDK's sidecar_exists,
save_sidecar and read_sidecar write and read one JSON document per content
and kind; write_sidecars in the manifest grants writing, read_sidecars or
the extension's own write kinds grant reading. save_sidecar drops its
extension id argument, since the host supplies the namespace.
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
* Extension data models: #[model] facets in an extension-owned store
#[model] derives a facet definition from the struct (strings and uuids,
integers, floats, booleans, datetimes; anything else as json) and
#[extension(models = [...])] declares each one to the host in plugin_init.
The host collects them into a DataTypeSchema and opens an sd-store database
under <library>/extensions/<extension id>/ on the first model operation:
the same record and facet shape as a source store, owned by the extension,
which is the store the extension-kinds plan registers as an app-owned
source later. create_model, get_model and update_model key a model by its
own uuid; the _for_content and _by_content forms key it by the content it
describes; query_models lists (filters are refused until a host side
exists). sd-store gains facet_rows, which reads a model's declared fields
back with their types.
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
* AI inference answers not_available; config.get; photos handles a missing detector
ctx.ai() calls go to the host as ai.infer with the model id, task and raw
input. The host checks the manifest's use_models grant and then, since the
core has no provider for face detection, scene classification, embeddings
or language models, answers not_available naming the category; nothing
builds inference here. analyze_photos handles it: one warning, the photo
keeps no faces sidecar so a later run with a provider picks it up, and the
job completes. config.get serves config.json beside the manifest (or {})
behind ctx.config::<C>(), which the clustering task reads. The photos
extension's DBSCAN and person matching are implemented over the model
store so the job runs to its end instead of a todo!().
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
* extension: fixture library and end-to-end acceptance for host functions
The test extension gains a catalog job that reads every record of the
library's sources, digests each file through a task, writes a digest
sidecar and a CatalogEntry model, and skips what a previous run already
digested. The acceptance suite builds a library with twelve JPEG-magic
files and two text files, runs catalog twice, then runs the photos
extension's analyze_photos over the same library and asserts the
not_available path: one warning, no faces sidecar, every task attempt
logged.
Checkpointed guest state now tolerates fields added since the
checkpoint was written (serde(default) on the state structs), and
SourceDb::facet_rows numbers every bind parameter: sqlx binds a bare ?
after a ?N by position, so the external id landed in LIMIT.
Regenerates the TypeScript types for the SidecarKind::Extension variant.
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
* docs: extension host functions phase 1
The extension README describes spacedrive_op, the operations table and
the model registry in place of permissions.rs and spacedrive_call. The
SDK reference records the Entry to Record rename, the save_sidecar
signature, what has a host side and what does not. The extension READMEs
show the manifest permissions shape and the record, sidecar and model
calls that run today. PROJECT_STATUS gains the phase 1 register row.
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
* extension: grant checks fail closed; guest identifiers validated
A read_records glob the parser cannot read used to mean no filter for
records.query and no access for records.get, so the manifest is refused
at load unless the glob ends in *.ext or *.{a,b}, and granted_extensions
names nothing for anything else. The loader also refuses an extension id
that is not one path-safe segment, since it becomes a directory under
the library for sidecars and the model store. register_model checks
every field name as an identifier (and refuses record_uuid, the facet
key) because the guest can hand it any JSON. spacedrive_random resolves
the guest slice before allocating the entropy buffer.
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
---------
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
Spacedrive Official Extensions
This directory contains the extension SDK and official extensions for Spacedrive.
Structure
extensions/
├── spacedrive-sdk/ # Core SDK library
├── spacedrive-sdk-macros/ # Proc macros for beautiful API
├── test-extension/ # Example extension with beautiful API
└── finance/ # (Future) First revenue-generating extension
Quick Start
1. Install WASM Target
rustup target add wasm32-unknown-unknown
2. Create Extension
cargo new --lib my-extension
cd my-extension
Cargo.toml:
[lib]
crate-type = ["cdylib"]
[dependencies]
spacedrive-sdk = { path = "../spacedrive-sdk" }
serde = { version = "1.0", features = ["derive"] }
src/lib.rs:
use spacedrive_sdk::prelude::*;
use spacedrive_sdk::{extension, job};
#[extension(
id = "my-extension",
name = "My Extension",
version = "0.1.0"
)]
struct MyExtension;
#[derive(Serialize, Deserialize, Default)]
pub struct MyJobState {
pub counter: u32,
}
#[job]
fn my_job(ctx: &JobContext, state: &mut MyJobState) -> Result<()> {
ctx.log("Job starting!");
state.counter += 1;
ctx.report_progress(1.0, "Done!");
Ok(())
}
3. Build
cargo build --target wasm32-unknown-unknown --release
cp target/wasm32-unknown-unknown/release/my_extension.wasm .
4. Create manifest.json
{
"id": "my-extension",
"name": "My Extension",
"version": "0.1.0",
"wasm_file": "my_extension.wasm",
"permissions": {
"read_records": { "glob": "**/*.{jpg,png}" },
"read_sidecars": ["exif"],
"write_sidecars": ["digest"],
"use_models": [{ "category": "face_detection", "preference": "local" }]
}
}
The host rejects a manifest with a field it does not know, so a typo in a
grant fails at load. read_records is the only way to see files; its optional
glob is honored for its trailing extension list only (*.jpg or
**/*.{jpg,png}); the directory part is ignored and a glob of another shape
is refused at load. write_sidecars names the kinds
the extension may write; use_models the inference categories it may ask for.
write_tags, write_custom_fields and dispatch_jobs are declared but have
no host side yet.
The Beautiful API
Before Macros (Manual FFI):
#[no_mangle]
pub extern "C" fn execute_my_job(
ctx_ptr: u32, ctx_len: u32,
state_ptr: u32, state_len: u32
) -> i32 {
let ctx_json = unsafe { /* 30 lines of pointer manipulation */ };
let mut state = /* 40 lines of deserialization */;
// ... business logic buried in boilerplate ...
}
180+ lines, lots of unsafe
After Macros (Beautiful):
#[job]
fn my_job(ctx: &JobContext, state: &mut MyJobState) -> Result<()> {
// Just write business logic!
ctx.log("Working...");
state.counter += 1;
Ok(())
}
60-80 lines, zero unsafe, pure logic
API Reference
Extension Container
#[extension(
id = "finance",
name = "Spacedrive Finance",
version = "0.1.0"
)]
struct Finance;
Generates:
plugin_init()exportplugin_cleanup()export- Metadata for manifest generation
Job Definition
#[job]
fn email_scan(ctx: &JobContext, state: &mut EmailScanState) -> Result<()> {
// Progress reporting
ctx.report_progress(0.5, "Half done");
// Checkpointing
ctx.checkpoint(state)?;
// Interruption handling
if ctx.check_interrupt() {
return Err(Error::OperationFailed("Interrupted".into()));
}
// Metrics
ctx.increment_items(1);
ctx.increment_bytes(1000);
// Warnings
ctx.add_warning("Non-fatal issue");
// Full SDK access
let record = ctx.vdfs().get_record(uuid).await?;
let bytes = record.read().await?;
Ok(())
}
VDFS Operations
// Records of the library's sources, filtered by extension (read_records grant)
let photos = ctx.vdfs()
.query_records()
.with_extensions(["jpg", "png"])
.collect()
.await?;
// One record and its bytes
let record = ctx.vdfs().get_record(uuid).await?;
let bytes = record.read().await?;
// Sidecars are one JSON document per kind, keyed by content uuid
let content_uuid = record.content_uuid().expect("hashed");
if !ctx.sidecar_exists(content_uuid, "digest")? {
ctx.save_sidecar(content_uuid, "digest", &digest).await?;
}
let digest: Digest = ctx.read_sidecar(content_uuid, "digest").await?;
// Models declared with #[model] live in the extension's own store
ctx.vdfs().create_model(entry).await?;
let all: Vec<CatalogEntry> = ctx.vdfs().query_models().collect().await?;
Tags and custom fields have no host side yet and return Error::NotAvailable.
AI Operations
Inference goes through ctx.ai().infer(...) behind the use_models grant.
The core has no provider yet, so every request answers Error::NotAvailable;
handle it the way extensions/photos/src/jobs/analyze.rs does, with one
warning and a skip, so the job completes and a later run with a provider
picks the files up.
// Written against the API; answers NotAvailable until a provider exists
let faces = ctx.ai().from_registered("face_detection").detect_faces(&bytes).await?;
Credentials
// Store OAuth
ctx.credentials().store("gmail", Credential::oauth2(
access_token,
Some(refresh_token),
3600,
vec!["https://www.googleapis.com/auth/gmail.readonly".into()]
))?;
// Get (auto-refreshes)
let cred = ctx.credentials().get("gmail")?;
Examples
See extensions/test-extension/ for a complete working example.
Building
All extensions:
cd extensions/test-extension
cargo build --target wasm32-unknown-unknown --release
Documentation
- SDK API Vision - Future API improvements
- Before/After Comparison - See the transformation
- WASM Architecture - Technical details
- Platform Revenue Model - Business case
Extension development is now beautiful, safe, and productive. Start building!