From 312dbf39b72ebb7aef76b0634c97a0803b7ea468 Mon Sep 17 00:00:00 2001 From: Jamie Pine <32987599+jamiepine@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:08:02 -0700 Subject: [PATCH] Extension host functions phase 1: photos analyze_photos runs end to end (#3124) * 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// 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 /extensions// 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::(), 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> --- Cargo.lock | 2 + PROJECT_STATUS.md | 1 + core/Cargo.toml | 1 + core/src/domain/sidecar.rs | 44 +- core/src/infra/extension/README.md | 63 +- core/src/infra/extension/host_functions.rs | 382 +++++------ core/src/infra/extension/manager.rs | 91 ++- core/src/infra/extension/mod.rs | 21 +- core/src/infra/extension/model_registry.rs | 128 ++++ core/src/infra/extension/ops.rs | 633 ++++++++++++++++++ core/src/infra/extension/permissions.rs | 203 ------ core/src/infra/extension/types.rs | 275 ++++++-- core/src/infra/extension/wasm_job.rs | 42 +- core/src/lib.rs | 6 +- core/src/ops/extensions/list.rs | 4 +- core/tests/wasm_extension_test.rs | 429 +++++++++--- crates/sdk-macros/src/extension.rs | 32 + crates/sdk-macros/src/model.rs | 64 +- crates/sdk-macros/src/task.rs | 127 +++- crates/sdk/.cargo/config.toml | 4 + crates/sdk/API_REFERENCE.md | 218 +++--- crates/sdk/Cargo.toml | 1 + crates/sdk/src/actions.rs | 4 +- crates/sdk/src/ai.rs | 33 +- crates/sdk/src/clock.rs | 16 + crates/sdk/src/config.rs | 17 + crates/sdk/src/ffi.rs | 134 +++- crates/sdk/src/job_context.rs | 55 +- crates/sdk/src/lib.rs | 33 +- crates/sdk/src/models.rs | 16 +- crates/sdk/src/tasks.rs | 104 ++- crates/sdk/src/types.rs | 103 ++- crates/sdk/src/vdfs.rs | 257 +++++-- crates/sidecar-path/src/lib.rs | 22 + crates/store/src/db.rs | 75 ++- extensions/README.md | 67 +- extensions/photos/.cargo/config.toml | 5 +- extensions/photos/README.md | 37 +- extensions/photos/manifest.json | 2 +- extensions/photos/photos.wasm | Bin 154721 -> 332460 bytes extensions/photos/src/actions/create_album.rs | 3 +- extensions/photos/src/agent/handlers.rs | 8 +- extensions/photos/src/jobs/analyze.rs | 48 +- extensions/photos/src/jobs/clustering.rs | 64 +- extensions/photos/src/jobs/moments.rs | 1 - extensions/photos/src/jobs/places.rs | 4 +- extensions/photos/src/jobs/scenes.rs | 7 +- extensions/photos/src/lib.rs | 1 + extensions/photos/src/models/photo.rs | 8 +- extensions/photos/src/queries/search_scene.rs | 9 +- extensions/photos/src/tasks/classify_scene.rs | 2 +- extensions/photos/src/tasks/detect_faces.rs | 2 +- extensions/photos/src/utils/clustering.rs | 98 ++- extensions/test-extension/.cargo/config.toml | 3 +- extensions/test-extension/Cargo.lock | 43 ++ extensions/test-extension/Cargo.toml | 2 + extensions/test-extension/manifest.json | 19 +- extensions/test-extension/src/lib.rs | 116 +++- extensions/test-extension/test_extension.wasm | Bin 82571 -> 192176 bytes packages/ts-client/src/generated/types.ts | 8 +- 60 files changed, 3170 insertions(+), 1027 deletions(-) create mode 100644 core/src/infra/extension/model_registry.rs create mode 100644 core/src/infra/extension/ops.rs delete mode 100644 core/src/infra/extension/permissions.rs create mode 100644 crates/sdk/.cargo/config.toml create mode 100644 crates/sdk/src/clock.rs create mode 100644 crates/sdk/src/config.rs diff --git a/Cargo.lock b/Cargo.lock index 341e9f287..c3005a455 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11666,6 +11666,7 @@ dependencies = [ "hound", "if-watch", "image", + "indexmap 2.11.4", "indicatif", "int-enum", "inventory", @@ -12988,6 +12989,7 @@ name = "spacedrive-sdk" version = "0.1.0" dependencies = [ "chrono", + "getrandom 0.4.3", "serde", "serde_json", "spacedrive-sdk-macros", diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index 826fed317..a97b29b36 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -211,6 +211,7 @@ Recent landed. | CLI and daemon housekeeping from the live runs | in progress 2026-10-05 (Fable, SPAC-16); one PR against `main`, one commit per item | Fable | Nine small fixes the Mac Studio / MacBook sessions (SPAC-9, 10, 12, 15) turned up. `core.status` defaults its `replication` block so a newer `sd --device status` reads an older daemon (unit test on the pre-#3109 shape); `sd config get/set/show` cover `replication.paused` beside `max_bytes_per_sec` and `--help` lists the keys; a peer syncing a library this device no longer holds gets one WARN per connection and a `SyncMessage::Error` reply so its catch-up fails fast and backs off (a real "leave library" is a separate brief); `core.status` reads paired and connected counts from the network trust registry so `sd status` and `sd network status` agree ("Paired devices"); `sd update` downloads into resumable `/updates/*.part` files with a 60 s idle timeout, verifies sha256 before replacing, appends every attempt with its reason to `logs/update.log`, keeps `update-state.json`, writes `sd-version.txt` beside the binaries, and `sd update status` prints the running build, marker, last attempt and result; the daemon plist asks launchd for 65536 file descriptors; `jobs.list` no longer loads each job's persisted `state` blob (an indexer over millions of files keeps its seen-path set there, which is why a remote `job list` took 45 s); `maximize-build-space` removed from the Blacksmith jobs in `release.yml` and `server.yml`. `main` is the branch of record from 2026-10-05 (fast-forwarded to `sources` at 10dbc18): `nightly.yml` and `core_tests.yml` now trigger on `main`, with `sources` kept in the lists for one release. Next: merge, nightly, re-run the SPAC-15 re-join once the MacBook is on it. | | Library backup and restore | in progress 2026-10-06 (Fable, SPAC-23); one PR against `main` | Fable | Release gate "Database backup and restore pass from release candidates" and candidate validation step 7. `libraries.backup` copies `library.db`, `sync.db`, every owned source store and its `sidecars.db` with `VACUUM INTO` on a fresh connection while the daemon runs, hashes each file with blake3 and writes `manifest.json` (device, build sha, applied migrations by name, each source's store id, revision and schema hash), into a directory or `.tar.zst`; `libraries.backup.verify` rehashes without restoring; `libraries.restore` verifies every hash first, refuses a backup whose library applied a migration this build does not know, stages under the data dir, renames into place (replace mode closes the library, releases open store and sidecar pools so the swapped `data.db` is what the next open reads, and parks the displaced state in `restore-trash/`), and reopens. Replacing a library other devices are members of needs `force`, because the device rows and sync watermarks in it would diverge from what those peers hold. CLI: `sd library backup --to`, `sd library backup verify --from`, `sd library restore --from [--as-new|--replace] [--force]`. `core/tests/library_backup_test.rs` backs up while a watcher writes, restores into a fresh data dir and proves counts, tag assertions, space items and store revisions equal, replaces over a mutated library and proves the mutation gone, and detects a one-byte tamper. Documented in `docs/core/library.mdx` (Backup and restore). Next: run the gate on the MacBook library before the beta candidate. | | Extension loading | in progress 2026-10-07 (Fable, SPAC-28); one PR against `main` | Fable | Product sequence step 5, first half. A daemon built with the `wasm` feature discovers every `/extensions//manifest.json` at startup (before libraries open, so a resumed job finds its extension), instantiates it with wasmer 6 (4.4 stopped linking on Rust 1.89+, wasmer #5610), and keeps the instance for its lifetime. `plugin_init` registers jobs; `extensions.list` reports them and `extensions.run_job` starts one as a `wasm_job` through the normal job manager. The guest's host calls are bridged to the job context: progress, warnings, metrics, an interrupt flag the pause path sets, and checkpoints that reach the checkpoint table before the guest continues, so both a graceful stop (paused row) and a SIGKILL (running row plus checkpoint) resume from the guest's last state on restart; proven live on a Linux daemon and in `core/tests/wasm_extension_test.rs` (acceptance group, with its own `--features wasm` build: carrying the feature across the integration group added minutes to every test binary link, 51 to 78 minutes on the first CI run). The SDK's `#[job]` export now runs the job function (async ones through a poll loop) and `plugin_init` installs a panic hook so a guest panic reaches the host log. Phase B: the photos extension builds to wasm32 without wasm-bindgen imports, loads beside the test extension and registers four jobs; its first job stops at `JobContext::run` (task execution has no host function), and the SDK has no file-kind or preview registration at all, so step 5's second half is a host-function brief (report SPAC-28 lists the gaps: task execution, vdfs entry and sidecar access, AI inference, model registry, tags, entropy, clock, file-kind and preview registration). The `wasm` feature stays opt-in; whether the shipped daemon turns it on is Jamie's call. | +| Extension host functions, phase 1 | in progress 2026-10-07 (Fable, SPAC-30); one PR against `main`, one commit per gap | Fable | Product sequence step 5, second half, in SPAC-28's gap order. Every guest request that returns data now goes through one import, `spacedrive_op(name, payload)`, answered on the job's async side with permission checked against a strict manifest (`read_records` with an optional glob, `read_sidecars`, `write_sidecars` per kind, `use_models` per category; unknown fields are refused at load). Landed: task execution (`#[task]` attempts bracketed by `task.begin`/`task.end` with the SDK's deadline and an attempt line in the job log), entropy and clock (`spacedrive_random` backs `getrandom` so `Uuid::new_v4` works in the guest; `spacedrive_now_ms` backs `clock::now`), records over source stores (`Record` replaces the SDK's `Entry`; `records.get/query/read` resolve through the library's sources and read bytes from the store root; tag and metadata filters are refused), sidecars as one JSON document per extension kind under `SidecarKind::Extension { extension_id, kind }`, models (`#[model]` emits a definition `register_model` collects; the registry builds a `DataTypeSchema` and opens an sd-store db at `/extensions//data.db`; `models.put/get/list` keyed by uuid or `content:`), `config.get` for `config.json` beside the manifest, and `ai.infer`, which checks the `use_models` grant and answers `not_available` because the core has no inference provider. The photos extension handles that with one warning and a skip, so `analyze_photos` completes over a fixture library of twelve JPEGs in `core/tests/wasm_extension_test.rs` (the test extension's new `catalog` job covers records, sidecars, models and tasks in the same suite). Still unbacked: inference itself (so scenes, places and moments have nothing to run on), tags, custom fields, `dispatch_jobs`, agents, actions, queries, and file-kind or preview registration (gap 9, untouched). | | Extension file kinds and previews | design proposed 2026-10-07 (Fable, SPAC-31); nothing built | unowned | Plan: `docs/plans/2026-10-07-extension-kinds-and-previews.md`. Product sequence step 5, second half, and SPAC-28 gap 9. A manifest `kinds` section (name, built-in parent, extensions, magic bytes, preview) folds into a layered `FileTypeRegistry::current()` the plugin manager swaps on load and unload; the content identity phase writes `content.kind` and a new `content.kind_name` (store schema v2), so a file keeps its kind name after the extension unloads and the client falls back to the parent renderer; `ContentRenderer.tsx` becomes a lookup in a renderer registry fed by `extensions.list`, with bundle previews served from `/extension/:id/*` under a plain `mount(el, ctx)` contract. Nothing is stored to migrate: `content.kind` is NULL on every row today and kinds are derived at read time. Five phases K1 to K5, about eight days, two PRs. Jamie decides: kinds stay one level deep under a built-in parent; conflicts resolve by load order and are reported, no picker yet. | | Repository release gates in CI | in progress 2026-10-07 (Fable, SPAC-26); one PR against `main` | Fable | `docs/core/releases.mdx` lists five repository gates; only formatting, clippy, typecheck and tests had jobs. `ci.yml` gains four more on every PR to `main`: Docs (bun 1.3.4, `docs/` frozen lockfile, `types:check` and `next build`), Generated types (`generate_typescript_types` plus `cargo check -p spacedrive` for the Tauri schemas, fails on a diff under `packages/ts-client/src/generated` or `apps/tauri/src-tauri/gen/schemas`, own rust-cache key), Secrets scan (pinned gitleaks over the PR's commits and the tree, allowlist in `.gitleaks.toml` including the AWS docs example key in `cloud_credentials.rs`), and License and notices (LICENSE is Apache-2.0, NOTICE exists and carries the same AGPL-3.0 cut-over date as README). Found and fixed on the way: the docs build failed on an unescaped `` in `cli/library-sync-setup.mdx`, and the committed Tauri schemas predated the updater plugin. The gates in `releases.mdx` now name their job. Next: confirm the four jobs green on the PR and the warm wall time under 10 min. | diff --git a/core/Cargo.toml b/core/Cargo.toml index 1c8b1afcb..02606fa4b 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -124,6 +124,7 @@ sd-task-system = { path = "../crates/task-system" } # The per-source store, one SQLite file per source sd-store = { path = "../crates/store" } +indexmap = { workspace = true, features = ["serde"] } # Archive system - indexes external data sources (emails, notes, etc.) sd-archive = { path = "../crates/archive" } diff --git a/core/src/domain/sidecar.rs b/core/src/domain/sidecar.rs index f5586c74f..accfe7d99 100644 --- a/core/src/domain/sidecar.rs +++ b/core/src/domain/sidecar.rs @@ -9,6 +9,10 @@ use serde::{Deserialize, Serialize}; use specta::Type; use std::fmt; +/// What a sidecar holds. The core kinds are closed; an extension declares +/// its own in its manifest (`write_sidecars`) and they live under the +/// extension's namespace, so two extensions' `faces` never collide and no +/// extension can write a core kind. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Type)] #[serde(rename_all = "snake_case")] pub enum SidecarKind { @@ -19,10 +23,31 @@ pub enum SidecarKind { Ocr, Transcript, GaussianSplat, + Extension { extension_id: String, kind: String }, } impl SidecarKind { - pub fn as_str(&self) -> &'static str { + /// A kind an extension declared. Both parts must be single path + /// segments of `[A-Za-z0-9._-]`, since they become directory names. + pub fn extension(extension_id: &str, kind: &str) -> Result { + for (what, value) in [("extension id", extension_id), ("kind", kind)] { + let safe = !value.is_empty() + && value != "." + && value != ".." + && value + .chars() + .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')); + if !safe { + return Err(format!("Invalid sidecar {what}: {value:?}")); + } + } + Ok(Self::Extension { + extension_id: extension_id.to_string(), + kind: kind.to_string(), + }) + } + + pub fn as_str(&self) -> &str { match self { Self::Thumb => "thumb", Self::Thumbstrip => "thumbstrip", @@ -31,14 +56,23 @@ impl SidecarKind { Self::Ocr => "ocr", Self::Transcript => "transcript", Self::GaussianSplat => "gaussian_splat", + Self::Extension { kind, .. } => kind, } } - pub fn directory(&self) -> &'static str { + /// The directory under a content's sidecar directory, relative to the + /// `sidecars` tree root. + pub fn directory(&self) -> String { // The layout crate owns the mapping so out-of-process readers resolve - // the same directories; every enum variant is a known kind string. - sd_sidecar_path::kind_directory(self.as_str()) - .expect("every SidecarKind maps to a directory") + // the same directories; every core variant is a known kind string. + match self { + Self::Extension { extension_id, kind } => { + sd_sidecar_path::extension_kind_directory(extension_id, kind) + } + core => sd_sidecar_path::kind_directory(core.as_str()) + .expect("every core SidecarKind maps to a directory") + .to_string(), + } } } diff --git a/core/src/infra/extension/README.md b/core/src/infra/extension/README.md index 34aad34b0..a9a8e24ae 100644 --- a/core/src/infra/extension/README.md +++ b/core/src/infra/extension/README.md @@ -13,17 +13,29 @@ reports `supported: false` and `extensions.run_job` is refused. keeps each plugin's store and instance for its lifetime. `PluginRuntime` runs one job export on the calling thread. - `host_functions.rs`: the functions a guest imports from the `spacedrive` - module. `spacedrive_log` and `register_job` serve `plugin_init`; the `job_*` - functions report into the job that is running through a `JobBridge`. + module. `spacedrive_log`, `register_job` and `register_model` serve + `plugin_init`; the `job_*` functions report into the running job through a + `JobBridge`; `spacedrive_random` and `spacedrive_now_ms` give the guest + entropy and wall-clock time; `spacedrive_op` carries every request that + returns data. +- `ops.rs`: the operations behind `spacedrive_op`, named by a string and + carried as bytes. `JobOps` holds the library, the manifest and the open + extension store for the job's duration and answers one request at a time, + because the guest blocks on each. Permission is checked here against the + manifest, never in the guest. - `job_registry.rs`: the jobs extensions registered, keyed `:`. +- `model_registry.rs`: the models each extension declared through + `register_model`. The registry turns them into one `DataTypeSchema` and opens + an sd-store database at `/extensions//data.db` the first + time a job touches a model. - `wasm_job.rs`: the one core job type that runs every extension job. It holds the guest's state as a JSON string and persists it through the normal checkpoint table, so a kill or a pause resumes from the guest's last checkpoint. -- `permissions.rs`, `types.rs`: the manifest and the capability model for - `spacedrive_call`, which routes a Wire method through the operation - registry. The SDK does not import it yet. +- `types.rs`: the manifest. Both the manifest and its `permissions` block + reject unknown fields, so a typo in a grant fails at load instead of + silently granting nothing. ## Running a job @@ -37,18 +49,43 @@ reports `supported: false` and `extensions.run_job` is refused. interrupts the job. Exit code 0 completes, 1 is interrupted, anything else fails. +## Operations + +A guest sends `spacedrive_op(name, payload)` and gets back bytes it allocated +through `wasm_alloc`: JSON for a result, or an `OpError` with a stable `code` +(`not_found`, `permission_denied`, `invalid_input`, `not_available`, `failed`) +that the SDK maps onto its own error type. + +| Operation | Grant | What it does | +| --- | --- | --- | +| `task.begin`, `task.end` | none | Bracket one `#[task]` attempt. The host writes an attempt line to the job log and refuses a task that outlives the SDK's deadline. | +| `records.get`, `records.query` | `read_records` | A record by uuid, or the records of the library's sources filtered by kind and extension. The optional `glob` on the grant is honored for its trailing extension list only (`*.jpg`, `**/*.{jpg,png}`); the directory part is ignored, and a glob of another shape is refused at load. Tag filters are refused. | +| `records.read` | `read_records` | The bytes of a record, read from its source store's root. | +| `sidecars.exists`, `sidecars.read` | `read_sidecars` | One JSON document under `/sidecars/`, keyed by content uuid and `SidecarKind::Extension { extension_id, kind }`. | +| `sidecars.write` | `write_sidecars` (per kind) | Writes that document. | +| `models.put`, `models.get`, `models.list` | none, models are the extension's own | A row in the extension's store, keyed by the model's uuid or `content:` for a content-scoped model. | +| `ai.infer` | `use_models` (per category) | Checks the grant, then answers `not_available`: the core has no inference provider yet. | +| `config.get` | none | `config.json` beside the manifest, or `null`. | + ## What the SDK can call today -`spacedrive_log`, `register_job`, `job_report_progress`, `job_checkpoint`, -`job_check_interrupt`, `job_add_warning`, `job_increment_items`, -`job_increment_bytes`. Everything else in `crates/sdk` (VDFS entries, sidecars, -AI, models, tags, tasks, agents, entropy, clock) has no host function and -panics in the guest; the panic reaches the host log and the job fails. +Everything in the table above, plus `spacedrive_log`, `register_job`, +`register_model`, `spacedrive_random`, `spacedrive_now_ms` and the `job_*` +functions (`job_report_progress`, `job_checkpoint`, `job_check_interrupt`, +`job_add_warning`, `job_increment_items`, `job_increment_bytes`). Tags, custom +fields, dispatching jobs, agents and file-kind or preview registration have no +host side; the SDK returns an error or panics in the guest, and the panic +reaches the host log and fails the job. ## Testing `core/tests/wasm_extension_test.rs` loads the committed fixtures `extensions/test-extension/test_extension.wasm` and -`extensions/photos/photos.wasm`. Rebuild a fixture from its crate with -`cargo build --release` (each crate's `.cargo/config.toml` selects -wasm32-unknown-unknown) and copy the artifact next to its manifest. +`extensions/photos/photos.wasm` against a library with twelve JPEG-magic +files and two text files. The test extension's `catalog` job exercises +records, sidecars, models and tasks; the photos extension's `analyze_photos` +runs end to end and takes the `not_available` path for face detection. +Rebuild a fixture from its crate with `cargo build --release` (each crate's +`.cargo/config.toml` selects wasm32-unknown-unknown) and copy the artifact +next to its manifest. The suite is its own xtask acceptance entry with the +`wasm` feature, because linking wasmer into every test binary is slow. diff --git a/core/src/infra/extension/host_functions.rs b/core/src/infra/extension/host_functions.rs index 1d842dbe4..023651727 100644 --- a/core/src/infra/extension/host_functions.rs +++ b/core/src/infra/extension/host_functions.rs @@ -1,161 +1,31 @@ //! WASM host functions //! -//! This module provides the bridge between WASM extensions and Spacedrive's -//! operation registry. The key function is `host_spacedrive_call()` which routes -//! generic Wire method calls to the existing `execute_json_operation()` function -//! used by daemon RPC. +//! The functions a guest imports from the `spacedrive` module. `spacedrive_log` +//! and `register_job` serve `plugin_init`; the `job_*` functions report into +//! the running job; `spacedrive_op` carries every operation that returns data. +//! +//! An operation is answered on the job's async side: the host function reads +//! the request out of guest memory, sends it through the `JobBridge`, blocks +//! the guest thread until the answer arrives, and writes the answer into +//! memory taken from the guest's own `wasm_alloc`. The guest frees it. One +//! convention for every operation keeps the import table small and the memory +//! handling in one place on each side. use std::sync::Arc; -use uuid::Uuid; -use wasmer::{FunctionEnvMut, Memory, MemoryView, WasmPtr}; - -use crate::{infra::daemon::rpc::RpcServer, Core}; - -use super::permissions::ExtensionPermissions; +use wasmer::{FunctionEnvMut, Memory, MemoryView, TypedFunction, WasmPtr}; /// Environment passed to all host functions pub struct PluginEnv { pub extension_id: String, - pub core_context: Arc, // Just context, not full Core! - pub api_dispatcher: Arc, // For creating sessions - pub permissions: ExtensionPermissions, pub memory: Memory, + /// The guest's allocator, for answers. Set once the instance exists. + pub alloc: Option>, pub job_registry: Arc, + pub model_registry: Arc, pub current_job: Option, } -/// THE MAIN HOST FUNCTION - Generic Wire RPC -/// -/// This is the ONLY function WASM extensions need to call Spacedrive operations. -/// It routes calls to the existing Wire operation registry. -/// -/// # Arguments -/// - `method_ptr`, `method_len`: Wire method string (e.g., "query:ai.ocr") -/// - `library_id_ptr`: 0 for None, or pointer to 16 UUID bytes -/// - `payload_ptr`, `payload_len`: JSON payload string -/// -/// # Returns -/// Pointer to result JSON string in WASM memory (or 0 on error) -pub fn host_spacedrive_call( - mut env: FunctionEnvMut, - method_ptr: WasmPtr, - method_len: u32, - library_id_ptr: u32, - payload_ptr: WasmPtr, - payload_len: u32, -) -> u32 { - let (plugin_env, mut store) = env.data_and_store_mut(); - - // Get memory view from environment - let memory = &plugin_env.memory; - let memory_view = memory.view(&store); - - // 1. Read method string from WASM memory - let method = match read_string_from_wasm(&memory_view, method_ptr, method_len) { - Ok(m) => m, - Err(e) => { - tracing::error!("Failed to read method string: {}", e); - return 0; - } - }; - - // 2. Read library_id (0 = None) - let library_id = if library_id_ptr == 0 { - None - } else { - match read_uuid_from_wasm(&memory_view, WasmPtr::new(library_id_ptr)) { - Ok(uuid) => Some(uuid), - Err(e) => { - tracing::error!("Failed to read library UUID: {}", e); - return 0; - } - } - }; - - // 3. Read payload JSON - let payload_str = match read_string_from_wasm(&memory_view, payload_ptr, payload_len) { - Ok(s) => s, - Err(e) => { - tracing::error!("Failed to read payload: {}", e); - return 0; - } - }; - - let payload_json: serde_json::Value = match serde_json::from_str(&payload_str) { - Ok(json) => json, - Err(e) => { - tracing::error!("Failed to parse payload JSON: {}", e); - return write_error_to_memory(&memory, &mut store, &format!("Invalid JSON: {}", e)); - } - }; - - // 4. Permission check - let auth_result = tokio::runtime::Handle::current() - .block_on(async { plugin_env.permissions.authorize(&method, library_id).await }); - - if let Err(e) = auth_result { - tracing::warn!( - extension = %plugin_env.extension_id, - method = %method, - "Permission denied: {}", - e - ); - return write_error_to_memory(&memory, &mut store, &format!("Permission denied: {}", e)); - } - - tracing::debug!( - extension = %plugin_env.extension_id, - method = %method, - library_id = ?library_id, - "Extension calling operation" - ); - - // 5. Call operation handlers directly (same as execute_json_operation does) - let result = tokio::runtime::Handle::current().block_on(async { - // Create base session - let base_session = match plugin_env.api_dispatcher.create_base_session() { - Ok(s) => s, - Err(e) => return Err(e), - }; - - // Try library queries - if let Some(handler) = crate::infra::wire::registry::LIBRARY_QUERIES.get(method.as_str()) { - let lib_id = library_id.ok_or_else(|| "Library ID required".to_string())?; - let session = base_session.with_library(lib_id); - return handler(plugin_env.core_context.clone(), session, payload_json).await; - } - - // Try core queries - if let Some(handler) = crate::infra::wire::registry::CORE_QUERIES.get(method.as_str()) { - return handler(plugin_env.core_context.clone(), base_session, payload_json).await; - } - - // Try library actions - if let Some(handler) = crate::infra::wire::registry::LIBRARY_ACTIONS.get(method.as_str()) { - let lib_id = library_id.ok_or_else(|| "Library ID required".to_string())?; - let session = base_session.with_library(lib_id); - return handler(plugin_env.core_context.clone(), session, payload_json).await; - } - - // Try core actions - if let Some(handler) = crate::infra::wire::registry::CORE_ACTIONS.get(method.as_str()) { - return handler(plugin_env.core_context.clone(), payload_json).await; - } - - Err(format!("Unknown method: {}", method)) - }); - - // 6. Write result to WASM memory - match result { - Ok(json) => write_json_to_memory(&memory, &mut store, &json), - Err(e) => { - tracing::error!("Operation failed: {}", e); - write_error_to_memory(&memory, &mut store, &e) - } - } -} - /// Optional logging helper for extensions pub fn host_spacedrive_log( mut env: FunctionEnvMut, @@ -201,74 +71,6 @@ fn read_string_from_wasm( String::from_utf8(bytes).map_err(|e| e.into()) } -fn read_uuid_from_wasm( - memory_view: &MemoryView, - ptr: WasmPtr, -) -> Result> { - let bytes = ptr - .slice(memory_view, 16) - .and_then(|slice| slice.read_to_vec()) - .map_err(|e| format!("Failed to read UUID from WASM memory: {:?}", e))?; - - let uuid_bytes: [u8; 16] = bytes - .try_into() - .map_err(|_| "Invalid UUID bytes (expected 16 bytes)")?; - - Ok(Uuid::from_bytes(uuid_bytes)) -} - -fn write_json_to_memory( - memory: &Memory, - store: &mut wasmer::StoreMut, - json: &serde_json::Value, -) -> u32 { - let json_str = match serde_json::to_string(json) { - Ok(s) => s, - Err(e) => { - tracing::error!("Failed to serialize JSON: {}", e); - return 0; // NULL indicates error - } - }; - - let bytes = json_str.as_bytes(); - - // Try to call guest's allocator function - // WASM module must export: fn wasm_alloc(size: i32) -> i32 - let alloc_result = memory - .view(&store) - .data_size() // Just check memory exists for now - .checked_sub(bytes.len() as u64); - - if alloc_result.is_none() { - tracing::error!("Not enough WASM memory for result"); - return 0; - } - - // For now, write to a fixed offset (will implement proper allocator later) - // This is a simplification for testing - production needs guest allocator - let result_offset = 65536u32; // Start at 64KB - - let memory_view = memory.view(&store); - let wasm_ptr = WasmPtr::::new(result_offset); - - if let Ok(slice) = wasm_ptr.slice(&memory_view, bytes.len() as u32) { - if let Err(e) = slice.write_slice(bytes) { - tracing::error!("Failed to write to WASM memory: {:?}", e); - return 0; - } - } else { - tracing::error!("Failed to get WASM memory slice"); - return 0; - } - - result_offset -} - -fn write_error_to_memory(memory: &Memory, store: &mut wasmer::StoreMut, error: &str) -> u32 { - let error_json = serde_json::json!({ "error": error }); - write_json_to_memory(memory, store, &error_json) -} - /// What a running job's host calls report back to its `WasmJob`. /// /// The guest runs on a blocking thread while the job's async side drains these @@ -285,6 +87,12 @@ pub enum JobEvent { Warning(String), Items(u64), Bytes(u64), + /// A request for data, answered by `super::ops` on the async side. + Op { + op: String, + payload: Vec, + reply: tokio::sync::oneshot::Sender, + }, } /// The job a plugin instance is running right now, if any. @@ -376,6 +184,123 @@ pub fn host_job_checkpoint( }) } +/// Run one data-returning operation for the job. +/// +/// Writes `[ptr, len]` of the answer to `out_ptr` and returns 0, or writes a +/// JSON error there and returns 1. Both buffers come from the guest's +/// `wasm_alloc`, which the guest frees after reading. Called outside a job, +/// or with an unreadable request, nothing is written and 2 is returned. +pub fn host_spacedrive_op( + mut env: FunctionEnvMut, + op_ptr: WasmPtr, + op_len: u32, + payload_ptr: WasmPtr, + payload_len: u32, + out_ptr: WasmPtr, +) -> i32 { + let (plugin_env, mut store) = env.data_and_store_mut(); + let memory = plugin_env.memory.clone(); + let memory_view = memory.view(&store); + + let op = match read_string_from_wasm(&memory_view, op_ptr, op_len) { + Ok(op) => op, + Err(e) => { + tracing::error!("Failed to read op name: {}", e); + return 2; + } + }; + let payload = match payload_ptr + .slice(&memory_view, payload_len) + .and_then(|slice| slice.read_to_vec()) + { + Ok(payload) => payload, + Err(e) => { + tracing::error!("Failed to read op payload: {:?}", e); + return 2; + } + }; + let Some(alloc) = plugin_env.alloc.clone() else { + return 2; + }; + + let answer = with_job(plugin_env, None, |job| { + let (reply_tx, reply_rx) = tokio::sync::oneshot::channel(); + if job + .events + .send(JobEvent::Op { + op: op.clone(), + payload, + reply: reply_tx, + }) + .is_err() + { + return None; + } + reply_rx.blocking_recv().ok() + }); + let Some(answer) = answer else { + return 2; + }; + + let (status, bytes) = match answer { + Ok(bytes) => (0, bytes), + Err(error) => (1, error.to_json().into_bytes()), + }; + + // The guest frees `len.max(1)` bytes, matching the allocation here. + let len = bytes.len() as u32; + let ptr = match alloc.call(&mut store, len.max(1) as i32) { + Ok(ptr) => ptr as u32, + Err(e) => { + tracing::error!(extension = %plugin_env.extension_id, op, "wasm_alloc failed: {e}"); + return 2; + } + }; + let memory_view = memory.view(&store); + if let Err(e) = memory_view.write(ptr as u64, &bytes) { + tracing::error!(extension = %plugin_env.extension_id, op, "write answer: {e}"); + return 2; + } + if let Err(e) = out_ptr + .slice(&memory_view, 2) + .and_then(|slice| slice.write_slice(&[ptr, len])) + { + tracing::error!(extension = %plugin_env.extension_id, op, "write out pointer: {e:?}"); + return 2; + } + status +} + +/// Fill guest memory with entropy. +/// +/// Every extension gets this without a grant: random ids and the time are +/// not library data. +pub fn host_spacedrive_random(mut env: FunctionEnvMut, buf_ptr: WasmPtr, len: u32) { + use rand::RngCore; + + let (plugin_env, store) = env.data_and_store_mut(); + let view = plugin_env.memory.view(&store); + // Resolve the guest slice before allocating, so a bogus length is an + // error for the guest rather than a host allocation of its choosing. + let slice = match buf_ptr.slice(&view, len) { + Ok(slice) => slice, + Err(e) => { + tracing::error!(extension = %plugin_env.extension_id, "write entropy: {e:?}"); + return; + } + }; + let mut bytes = vec![0u8; len as usize]; + rand::thread_rng().fill_bytes(&mut bytes); + if let Err(e) = slice.write_slice(&bytes) { + tracing::error!(extension = %plugin_env.extension_id, "write entropy: {e:?}"); + } +} + +/// The host's clock, as milliseconds since the Unix epoch. +pub fn host_spacedrive_now_ms(_env: FunctionEnvMut) -> i64 { + chrono::Utc::now().timestamp_millis() +} + /// Check if job should be interrupted pub fn host_job_check_interrupt( mut env: FunctionEnvMut, @@ -495,3 +420,36 @@ pub fn host_register_job( } } } + +/// Declare a data model's facet, from `plugin_init`. +/// +/// The definition is the JSON the SDK's `#[model]` macro derived from the +/// struct. Returns 0 on success, 1 on error. +pub fn host_register_model( + mut env: FunctionEnvMut, + def_ptr: WasmPtr, + def_len: u32, +) -> i32 { + let (plugin_env, store) = env.data_and_store_mut(); + let memory_view = plugin_env.memory.view(&store); + let definition = match read_string_from_wasm(&memory_view, def_ptr, def_len) + .map_err(|e| e.to_string()) + .and_then(|json| serde_json::from_str(&json).map_err(|e| e.to_string())) + { + Ok(definition) => definition, + Err(e) => { + tracing::error!(extension = %plugin_env.extension_id, "Unreadable model definition: {e}"); + return 1; + } + }; + match plugin_env + .model_registry + .register(&plugin_env.extension_id, definition) + { + Ok(()) => 0, + Err(e) => { + tracing::error!(extension = %plugin_env.extension_id, "Failed to register model: {e}"); + 1 + } + } +} diff --git a/core/src/infra/extension/manager.rs b/core/src/infra/extension/manager.rs index c28247225..1d886e3a2 100644 --- a/core/src/infra/extension/manager.rs +++ b/core/src/infra/extension/manager.rs @@ -13,13 +13,9 @@ use thiserror::Error; use tokio::sync::RwLock; use wasmer::{imports, Function, FunctionEnv, Instance, Memory, Module, Store, TypedFunction}; -use crate::{context::CoreContext, infra::api::ApiDispatcher}; - -use super::host_functions::{ - self, host_spacedrive_call, host_spacedrive_log, JobBridge, PluginEnv, -}; +use super::host_functions::{self, host_spacedrive_log, JobBridge, PluginEnv}; use super::job_registry::ExtensionJobRegistry; -use super::permissions::ExtensionPermissions; +use super::model_registry::ExtensionModelRegistry; use super::types::{ExtensionManifest, LoadedPlugin}; #[derive(Error, Debug)] @@ -137,24 +133,18 @@ impl PluginRuntime { pub struct PluginManager { plugins: Arc>>, plugin_dir: PathBuf, - core_context: Arc, - api_dispatcher: Arc, job_registry: Arc, + model_registry: Arc, } impl PluginManager { /// Create new plugin manager - pub fn new( - plugin_dir: PathBuf, - core_context: Arc, - api_dispatcher: Arc, - ) -> Self { + pub fn new(plugin_dir: PathBuf) -> Self { Self { plugins: Arc::new(RwLock::new(HashMap::new())), plugin_dir, - core_context, - api_dispatcher, job_registry: Arc::new(ExtensionJobRegistry::new()), + model_registry: Arc::new(ExtensionModelRegistry::new()), } } @@ -163,6 +153,11 @@ impl PluginManager { self.job_registry.clone() } + /// The data models extensions declared + pub fn model_registry(&self) -> Arc { + self.model_registry.clone() + } + /// The directory extensions are installed under: `/extensions`. pub fn plugin_dir(&self) -> &PathBuf { &self.plugin_dir @@ -231,6 +226,9 @@ impl PluginManager { PluginError::ManifestLoadFailed(format!("Failed to parse manifest: {}", e)) })? }; + manifest + .validate() + .map_err(PluginError::ManifestLoadFailed)?; let plugin_id = manifest.id.clone(); tracing::info!( @@ -251,9 +249,6 @@ impl PluginManager { PluginError::CompilationFailed(format!("Failed to compile WASM: {}", e)) })?; - let permissions = - ExtensionPermissions::from_manifest(manifest.id.clone(), &manifest.permissions); - // Placeholder memory until the instance exists; host functions only run // after it is swapped for the real export. let temp_memory = Memory::new(&mut store, wasmer::MemoryType::new(1, None, false)) @@ -263,11 +258,10 @@ impl PluginManager { let plugin_env = PluginEnv { extension_id: manifest.id.clone(), - core_context: self.core_context.clone(), - api_dispatcher: self.api_dispatcher.clone(), - permissions, memory: temp_memory, + alloc: None, job_registry: self.job_registry.clone(), + model_registry: self.model_registry.clone(), current_job: None, }; @@ -275,11 +269,6 @@ impl PluginManager { let import_object = imports! { "spacedrive" => { - "spacedrive_call" => Function::new_typed_with_env( - &mut store, - &env, - host_spacedrive_call - ), "spacedrive_log" => Function::new_typed_with_env( &mut store, &env, @@ -320,6 +309,26 @@ impl PluginManager { &env, host_functions::host_register_job ), + "register_model" => Function::new_typed_with_env( + &mut store, + &env, + host_functions::host_register_model + ), + "spacedrive_random" => Function::new_typed_with_env( + &mut store, + &env, + host_functions::host_spacedrive_random + ), + "spacedrive_now_ms" => Function::new_typed_with_env( + &mut store, + &env, + host_functions::host_spacedrive_now_ms + ), + "spacedrive_op" => Function::new_typed_with_env( + &mut store, + &env, + host_functions::host_spacedrive_op + ), } }; @@ -331,11 +340,16 @@ impl PluginManager { PluginError::InstantiationFailed(format!("Plugin missing memory export: {}", e)) })?; env.as_mut(&mut store).memory = memory.clone(); + let alloc: TypedFunction = instance + .exports + .get_typed_function(&store, "wasm_alloc") + .map_err(|e| PluginError::BadExport(format!("wasm_alloc: {e}")))?; + env.as_mut(&mut store).alloc = Some(alloc); Ok(LoadedPlugin { id: plugin_id, dir_name: dir_name.to_string(), - manifest, + manifest: Arc::new(manifest), loaded_at: Utc::now(), poisoned: poisoned.clone(), runtime: Arc::new(Mutex::new(PluginRuntime { @@ -361,6 +375,7 @@ impl PluginManager { Ok(init_fn) => { if let Err(e) = init_fn.call(&mut runtime.store, &[]) { self.job_registry.unregister_extension_jobs(&plugin_id); + self.model_registry.unregister_extension(&plugin_id); return Err(PluginError::InstantiationFailed(format!( "plugin_init() failed: {}", e @@ -395,6 +410,7 @@ impl PluginManager { .remove(plugin_id) .ok_or_else(|| PluginError::NotFound(plugin_id.to_string()))?; self.job_registry.unregister_extension_jobs(plugin_id); + self.model_registry.unregister_extension(plugin_id); // A trapped guest cannot be trusted to run cleanup, and a guest still // running a job keeps its instance alive until that job returns; the @@ -427,6 +443,7 @@ impl PluginManager { .ok_or_else(|| PluginError::NotFound(plugin_id.to_string()))?; let fresh = self.instantiate(&dir_name).await?; let previous_jobs = self.job_registry.list_jobs_for_extension(plugin_id); + let previous_schema = self.model_registry.schema_for(plugin_id); let previous = self.unload_plugin(plugin_id).await?; if let Err(e) = self.activate(fresh).await { // The replacement's plugin_init failed; put the old plugin back so @@ -439,6 +456,15 @@ impl PluginManager { job.resumable, ); } + for (name, def) in previous_schema.map(|s| s.models).unwrap_or_default() { + let _ = self.model_registry.register( + plugin_id, + super::model_registry::ModelDefinition { + name, + fields: def.fields, + }, + ); + } self.plugins .write() .await @@ -454,7 +480,7 @@ impl PluginManager { } /// Get plugin manifest - pub async fn get_manifest(&self, plugin_id: &str) -> Option { + pub async fn get_manifest(&self, plugin_id: &str) -> Option> { self.plugins .read() .await @@ -462,6 +488,15 @@ impl PluginManager { .map(|p| p.manifest.clone()) } + /// The directory a loaded plugin was installed from. + pub async fn plugin_path(&self, plugin_id: &str) -> Option { + self.plugins + .read() + .await + .get(plugin_id) + .map(|p| self.plugin_dir.join(&p.dir_name)) + } + /// The runtime of a loaded plugin, for running one of its jobs. pub async fn runtime(&self, plugin_id: &str) -> Option>> { self.plugins diff --git a/core/src/infra/extension/mod.rs b/core/src/infra/extension/mod.rs index f9384482f..58cd8ecd0 100644 --- a/core/src/infra/extension/mod.rs +++ b/core/src/infra/extension/mod.rs @@ -6,16 +6,17 @@ //! //! ## Architecture //! -//! Extensions communicate with Spacedrive Core via a minimal host function API. -//! The key insight: we expose ONE generic `spacedrive_call()` function that routes -//! to the existing Wire operation registry, reusing all daemon RPC infrastructure. +//! Extensions communicate with Spacedrive Core through a small set of host +//! functions. Everything that returns data goes through one of them, +//! `spacedrive_op`, whose operations `ops` answers on the running job's async +//! side with the manifest's grants checked there. //! //! ## Components //! //! - `manager`: Plugin lifecycle management (load, unload, hot-reload) -//! - `host_functions`: WASM host functions (bridge to operation registry) -//! - `permissions`: Capability-based security model -//! - `types`: Shared types and manifest format +//! - `host_functions`: WASM host functions +//! - `ops`: the operations behind `spacedrive_op` +//! - `types`: the manifest, its permission grants, and the loaded plugin #[cfg(feature = "wasm")] mod host_functions; @@ -24,7 +25,9 @@ mod job_registry; #[cfg(feature = "wasm")] mod manager; #[cfg(feature = "wasm")] -mod permissions; +mod model_registry; +#[cfg(feature = "wasm")] +mod ops; #[cfg(feature = "wasm")] mod types; #[cfg(feature = "wasm")] @@ -35,8 +38,8 @@ pub use job_registry::{ExtensionJobRegistration, ExtensionJobRegistry}; #[cfg(feature = "wasm")] pub use manager::PluginManager; #[cfg(feature = "wasm")] -pub use permissions::{ExtensionPermissions, PermissionError}; +pub use model_registry::{ExtensionModelRegistry, ModelDefinition}; #[cfg(feature = "wasm")] -pub use types::{ExtensionManifest, PluginManifest}; +pub use types::{ExtensionManifest, ManifestPermissions, PluginManifest}; #[cfg(feature = "wasm")] pub use wasm_job::WasmJob; diff --git a/core/src/infra/extension/model_registry.rs b/core/src/infra/extension/model_registry.rs new file mode 100644 index 000000000..2284f272a --- /dev/null +++ b/core/src/infra/extension/model_registry.rs @@ -0,0 +1,128 @@ +//! The data models extensions declared, and the store that holds them. +//! +//! `plugin_init` registers each `#[model]` through `register_model`; the +//! registry turns an extension's models into one `DataTypeSchema`, which is +//! what opens the extension's store in a library. The store is an ordinary +//! `sd_store` database under `/extensions//`: the +//! same record and facet shape as a source store, owned by the extension, +//! and the thing the extension-kinds plan registers as an app-owned source +//! when that phase lands. + +use std::collections::HashMap; +use std::path::Path; +use std::sync::RwLock; + +use indexmap::IndexMap; +use sd_store::{DataTypeSchema, FieldType, ModelDef, SourceDb, SourceManager}; +use serde::Deserialize; + +/// A model as the SDK's `#[model]` macro declares it. +#[derive(Debug, Clone, Deserialize)] +pub struct ModelDefinition { + pub name: String, + pub fields: IndexMap, +} + +#[derive(Default)] +pub struct ExtensionModelRegistry { + models: RwLock>>, +} + +impl ExtensionModelRegistry { + pub fn new() -> Self { + Self::default() + } + + /// Accept a model a guest declared. + /// + /// The name becomes a table and each field a column, spliced into DDL + /// and into the facet reader's SQL, and the guest can hand `register_model` + /// any JSON it likes, so every identifier is checked here rather than + /// trusted from the macro. + pub fn register(&self, extension_id: &str, model: ModelDefinition) -> Result<(), String> { + let identifier = |s: &str| { + !s.is_empty() + && !s.starts_with(|c: char| c.is_ascii_digit()) + && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') + }; + if !identifier(&model.name) { + return Err(format!( + "model name {:?} is not a valid table name", + model.name + )); + } + if let Some(field) = model.fields.keys().find(|f| !identifier(f)) { + return Err(format!( + "model {} field {field:?} is not a valid column name", + model.name + )); + } + if model.fields.contains_key("record_uuid") { + return Err(format!( + "model {} field \"record_uuid\" is the facet table's key column", + model.name + )); + } + let mut models = self.models.write().unwrap_or_else(|p| p.into_inner()); + let list = models.entry(extension_id.to_string()).or_default(); + if list.iter().any(|m| m.name == model.name) { + return Err(format!("model {} is already registered", model.name)); + } + tracing::info!(extension = extension_id, model = %model.name, "Registered extension model"); + list.push(model); + Ok(()) + } + + pub fn unregister_extension(&self, extension_id: &str) { + self.models + .write() + .unwrap_or_else(|p| p.into_inner()) + .remove(extension_id); + } + + /// The schema of an extension's store, or `None` when it declared no + /// models and so needs no store. + pub fn schema_for(&self, extension_id: &str) -> Option { + let models = self.models.read().unwrap_or_else(|p| p.into_inner()); + let declared = models.get(extension_id).filter(|m| !m.is_empty())?; + let mut defs = IndexMap::new(); + for model in declared { + defs.insert( + model.name.clone(), + ModelDef { + fields: model.fields.clone(), + relations: Default::default(), + }, + ); + } + Some(DataTypeSchema { + data_type: sd_store::schema::DataTypeMeta { + id: extension_id.to_string(), + name: extension_id.to_string(), + icon: None, + }, + search: sd_store::schema::SearchContract { + primary_model: declared[0].name.clone(), + title: "_derived.title".to_string(), + preview: "_derived.none".to_string(), + subtitle: None, + search_fields: Vec::new(), + date_field: None, + }, + models: defs, + }) + } +} + +/// Open an extension's store in a library, creating it on first use and +/// widening its facets when the extension declared new fields. +pub async fn open_extension_store( + library_path: &Path, + extension_id: &str, + schema: &DataTypeSchema, +) -> anyhow::Result { + let manager = SourceManager::new(library_path.join("extensions")); + manager.create(extension_id, schema).await?; + let (db, _) = manager.open_with_migration(extension_id, schema).await?; + Ok(db) +} diff --git a/core/src/infra/extension/ops.rs b/core/src/infra/extension/ops.rs new file mode 100644 index 000000000..aa0ce875a --- /dev/null +++ b/core/src/infra/extension/ops.rs @@ -0,0 +1,633 @@ +//! The operations a guest asks the host for through `spacedrive_op`. +//! +//! Every operation that returns data to a guest lands here, named by a +//! string and carried as bytes. Requests are answered on the job's async +//! side, one at a time, because the guest blocks on each; so the state an +//! operation needs lives on [`JobOps`] for the job's duration and no +//! operation ever contends with another from the same job. +//! +//! Permission is checked here against the manifest, never in the guest. + +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use sd_store::read::Start; +use sd_store::{FsEntry, SourceDb}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::domain::sidecar::{SidecarFormat, SidecarKind, SidecarVariant}; +use crate::infra::job::prelude::JobContext; +use crate::library::Library; +use crate::ops::indexing::store::SourceStore; + +use super::model_registry::{open_extension_store, ExtensionModelRegistry}; +use super::types::ExtensionManifest; + +/// An error a guest can act on. `code` is the stable part: the SDK maps it +/// onto its own error type, and `message` is for the log. +#[derive(Debug, Clone, Serialize)] +pub struct OpError { + pub code: &'static str, + pub message: String, +} + +impl OpError { + pub fn not_found() -> Self { + Self::new("not_found", "not found") + } + + pub fn permission_denied(message: impl Into) -> Self { + Self::new("permission_denied", message) + } + + pub fn invalid_input(message: impl Into) -> Self { + Self::new("invalid_input", message) + } + + pub fn not_available(message: impl Into) -> Self { + Self::new("not_available", message) + } + + pub fn failed(message: impl Into) -> Self { + Self::new("failed", message) + } + + fn new(code: &'static str, message: impl Into) -> Self { + Self { + code, + message: message.into(), + } + } + + pub fn to_json(&self) -> String { + serde_json::to_string(self) + .unwrap_or_else(|_| format!(r#"{{"code":"{}","message":"unserializable"}}"#, self.code)) + } +} + +pub type OpResult = Result, OpError>; + +/// A task attempt the guest is inside, as `task.begin` declared it. +struct ActiveTask { + name: String, + attempt: u32, + started: Instant, + deadline: Option, +} + +/// The host side of one running extension job. +pub struct JobOps { + pub extension_id: String, + pub manifest: Arc, + pub library: Arc, + /// The directory the extension was installed from, for `config.json`. + plugin_path: std::path::PathBuf, + models: Arc, + /// The extension's store in this library, opened on the first model + /// operation and closed when the job ends. + store: Option, + task: Option, +} + +#[derive(Deserialize)] +struct ModelRef { + model: String, + uuid: Option, + content_uuid: Option, +} + +#[derive(Deserialize)] +struct ModelPut { + model: String, + uuid: Option, + content_uuid: Option, + data: serde_json::Value, +} + +#[derive(Deserialize)] +struct Inference { + model: String, + task: String, +} + +#[derive(Deserialize)] +struct ModelList { + model: String, + limit: Option, +} + +/// A record as the SDK's `Record` deserializes it. +#[derive(Serialize)] +struct RecordOut { + uuid: Uuid, + source_id: Uuid, + name: String, + kind: &'static str, + extension: Option, + relative_path: String, + size: Option, + modified_ms: Option, + content_uuid: Option, +} + +impl RecordOut { + fn new(source_id: Uuid, entry: FsEntry) -> Self { + Self { + uuid: entry.uuid, + source_id, + name: entry.name, + kind: entry.kind.as_str(), + extension: entry.extension.map(|e| e.to_lowercase()), + relative_path: entry.relative_path, + size: entry.size.and_then(|s| u64::try_from(s).ok()), + modified_ms: entry.mtime_ms, + content_uuid: entry.content_uuid, + } + } +} + +#[derive(Deserialize)] +struct RecordRef { + uuid: Uuid, +} + +#[derive(Deserialize)] +struct RecordQuery { + source: Option, + scope: Option, + extensions: Option>, + tag: Option, + limit: Option, +} + +/// The most records one query answers with. +const QUERY_CAP: usize = 10_000; + +#[derive(Deserialize)] +struct SidecarRef { + content_uuid: Uuid, + kind: String, +} + +#[derive(Deserialize)] +struct SidecarWrite { + content_uuid: Uuid, + kind: String, + data: serde_json::Value, +} + +#[derive(Deserialize)] +struct TaskBegin { + name: String, + attempt: u32, + retries: u32, + timeout_ms: u64, +} + +#[derive(Deserialize)] +struct TaskEnd { + name: String, + attempt: u32, + error: Option, +} + +impl JobOps { + pub fn new( + extension_id: String, + manifest: Arc, + library: Arc, + plugin_path: std::path::PathBuf, + models: Arc, + ) -> Self { + Self { + extension_id, + manifest, + library, + plugin_path, + models, + store: None, + task: None, + } + } + + /// Release what the job held open. + pub async fn finish(self) { + if let Some(store) = self.store { + store.pool().close().await; + } + } + + /// Answer one request. + /// + /// Once the active task's deadline has passed, every operation but the + /// one that ends the task fails with `timeout`, which is how a + /// single-threaded guest gets stopped mid-task: it cannot be preempted, + /// but it cannot make progress without the host either. + pub async fn handle(&mut self, op: &str, payload: &[u8], ctx: &JobContext<'_>) -> OpResult { + if op != "task.end" { + if let Some(task) = &self.task { + if task + .deadline + .is_some_and(|deadline| Instant::now() >= deadline) + { + return Err(OpError::new( + "timeout", + format!("task {} ran past its deadline", task.name), + )); + } + } + } + + match op { + "task.begin" => self.task_begin(parse(payload)?, ctx), + "task.end" => self.task_end(parse(payload)?, ctx), + "records.get" => self.record_get(parse(payload)?).await, + "records.read" => self.record_read(parse(payload)?).await, + "records.query" => self.record_query(parse(payload)?).await, + "sidecars.exists" => self.sidecar_exists(parse(payload)?).await, + "sidecars.read" => self.sidecar_read(parse(payload)?).await, + "sidecars.write" => self.sidecar_write(parse(payload)?).await, + "models.put" => self.model_put(parse(payload)?).await, + "models.get" => self.model_get(parse(payload)?).await, + "models.list" => self.model_list(parse(payload)?).await, + "ai.infer" => self.ai_infer(payload), + "config.get" => self.config_get().await, + _ => Err(OpError::new( + "unknown_op", + format!("unknown operation {op}"), + )), + } + } + + fn task_begin(&mut self, begin: TaskBegin, ctx: &JobContext<'_>) -> OpResult { + if let Some(active) = &self.task { + return Err(OpError::invalid_input(format!( + "task {} started while task {} is running", + begin.name, active.name + ))); + } + ctx.log(format!( + "task {} attempt {}/{} started", + begin.name, + begin.attempt + 1, + begin.retries + 1 + )); + let started = Instant::now(); + self.task = Some(ActiveTask { + name: begin.name, + attempt: begin.attempt, + started, + deadline: (begin.timeout_ms > 0) + .then(|| started + Duration::from_millis(begin.timeout_ms)), + }); + json(&serde_json::json!({ "id": begin.attempt })) + } + + fn task_end(&mut self, end: TaskEnd, ctx: &JobContext<'_>) -> OpResult { + let Some(task) = self.task.take() else { + return Err(OpError::invalid_input(format!( + "task {} ended without starting", + end.name + ))); + }; + let elapsed = task.started.elapsed().as_millis(); + match end.error { + None => ctx.log(format!( + "task {} attempt {} finished in {elapsed} ms", + task.name, + task.attempt + 1 + )), + Some(error) => ctx.log(format!( + "task {} attempt {} failed after {elapsed} ms: {error}", + task.name, + task.attempt + 1 + )), + } + json(&serde_json::Value::Null) + } +} + +impl JobOps { + /// The stores of this library's sources, detached ones included: their + /// records are still true even when their bytes are out of reach. + async fn stores(&self) -> Vec> { + let index = self.library.core_context().volume_index(); + let mut stores = Vec::new(); + for source in index.sources_of(self.library.id()) { + if let Some(store) = index.store_for(&source.root).await { + stores.push(store); + } + } + stores + } + + /// The store holding a record, with the entry. + async fn locate(&self, uuid: Uuid) -> Result<(Arc, FsEntry), OpError> { + for store in self.stores().await { + match store.db().entry_by_uuid(uuid).await { + Ok(Some(entry)) => return Ok((store, entry)), + Ok(None) => {} + Err(error) => { + tracing::warn!(source = %store.id(), %error, "record lookup failed") + } + } + } + Err(OpError::not_found()) + } + + fn check_read(&self, entry: &FsEntry) -> Result<(), OpError> { + let extension = entry.extension.as_deref().map(str::to_lowercase); + if self + .manifest + .permissions + .can_read_record(extension.as_deref()) + { + return Ok(()); + } + Err(OpError::permission_denied(format!( + "{} may not read {}", + self.extension_id, entry.relative_path + ))) + } + + async fn record_get(&self, record: RecordRef) -> OpResult { + let (store, entry) = self.locate(record.uuid).await?; + self.check_read(&entry)?; + json(&RecordOut::new(store.id(), entry)) + } + + /// A record's bytes, through the path its source resolves. A detached + /// source answers not found: the record is still known, the bytes are + /// not here. + async fn record_read(&self, record: RecordRef) -> OpResult { + let (store, entry) = self.locate(record.uuid).await?; + self.check_read(&entry)?; + let path = store.root().join(&entry.relative_path); + tokio::fs::read(&path).await.map_err(|e| match e.kind() { + std::io::ErrorKind::NotFound => OpError::not_found(), + _ => OpError::failed(format!("read {}: {e}", path.display())), + }) + } + + /// Files across the library's stores, narrowed by the query and by the + /// grant: a glob grant restricts the extensions a query without its own + /// list gets back. + async fn record_query(&self, query: RecordQuery) -> OpResult { + if query.tag.is_some() { + return Err(OpError::invalid_input( + "record queries cannot filter by tag yet", + )); + } + let permissions = &self.manifest.permissions; + if permissions.read_records.is_none() { + return Err(OpError::permission_denied(format!( + "{} has no read_records grant", + self.extension_id + ))); + } + let extensions: Option> = match query.extensions { + Some(asked) => Some( + asked + .into_iter() + .filter(|e| permissions.can_read_record(Some(e))) + .collect(), + ), + None => permissions.granted_extensions(), + }; + let limit = query.limit.unwrap_or(QUERY_CAP).min(QUERY_CAP); + let scope = query.scope.unwrap_or_default(); + + let mut records = Vec::new(); + for store in self.stores().await { + if query.source.is_some_and(|source| source != store.id()) { + continue; + } + if records.len() >= limit { + break; + } + let entries = sd_store::read::files_beneath( + store.db().pool(), + &scope, + Start::First, + extensions.as_deref(), + false, + limit - records.len(), + ) + .await + .map_err(|e| OpError::failed(e.to_string()))?; + records.extend( + entries + .into_iter() + .map(|entry| RecordOut::new(store.id(), entry)), + ); + } + json(&records) + } +} + +impl JobOps { + /// Where a sidecar of one of this extension's kinds lives: the library's + /// content-addressed sidecar tree, under the extension's namespace. The + /// SDK's sidecars are JSON documents with one variant. + fn sidecar_path( + &self, + content_uuid: Uuid, + kind: &str, + write: bool, + ) -> Result { + let permissions = &self.manifest.permissions; + let granted = if write { + permissions.can_write_sidecar(kind) + } else { + permissions.can_read_sidecar(kind) + }; + if !granted { + return Err(OpError::permission_denied(format!( + "{} may not {} sidecar kind {kind}", + self.extension_id, + if write { "write" } else { "read" } + ))); + } + let kind = + SidecarKind::extension(&self.extension_id, kind).map_err(OpError::invalid_input)?; + Ok(self + .library + .path() + .join("sidecars") + .join(sd_sidecar_path::relative_path( + &content_uuid, + &kind.directory(), + SidecarVariant::new("default").as_str(), + SidecarFormat::Json.extension(), + ))) + } + + async fn sidecar_exists(&self, sidecar: SidecarRef) -> OpResult { + let path = self.sidecar_path(sidecar.content_uuid, &sidecar.kind, false)?; + json(&tokio::fs::try_exists(&path).await.unwrap_or(false)) + } + + async fn sidecar_read(&self, sidecar: SidecarRef) -> OpResult { + let path = self.sidecar_path(sidecar.content_uuid, &sidecar.kind, false)?; + tokio::fs::read(&path).await.map_err(|e| match e.kind() { + std::io::ErrorKind::NotFound => OpError::not_found(), + _ => OpError::failed(format!("read {}: {e}", path.display())), + }) + } + + /// Written whole and renamed into place, so a reader never sees a + /// partial document and a crash leaves either the old one or the new. + async fn sidecar_write(&self, sidecar: SidecarWrite) -> OpResult { + let path = self.sidecar_path(sidecar.content_uuid, &sidecar.kind, true)?; + let bytes = + serde_json::to_vec(&sidecar.data).map_err(|e| OpError::failed(e.to_string()))?; + let io = |e: std::io::Error| OpError::failed(format!("write {}: {e}", path.display())); + if let Some(parent) = path.parent() { + tokio::fs::create_dir_all(parent).await.map_err(io)?; + } + let tmp = path.with_extension("json.tmp"); + tokio::fs::write(&tmp, &bytes).await.map_err(io)?; + tokio::fs::rename(&tmp, &path).await.map_err(io)?; + json(&serde_json::Value::Null) + } +} + +impl JobOps { + /// The extension's store in this library, opened on first use. + async fn store(&mut self) -> Result<&SourceDb, OpError> { + if self.store.is_none() { + let schema = self.models.schema_for(&self.extension_id).ok_or_else(|| { + OpError::invalid_input(format!( + "{} declares no models; list them in #[extension(models = [...])]", + self.extension_id + )) + })?; + let db = open_extension_store(self.library.path(), &self.extension_id, &schema) + .await + .map_err(|e| OpError::failed(format!("open extension store: {e}")))?; + self.store = Some(db); + } + Ok(self.store.as_ref().expect("opened above")) + } + + /// A standalone model is keyed by its own uuid, a content-scoped one by + /// the content it describes. Both are the record's external id. + fn model_key(uuid: Option, content_uuid: Option) -> Result { + match (uuid, content_uuid) { + (Some(uuid), None) => Ok(uuid.to_string()), + (None, Some(content)) => Ok(format!("content:{content}")), + _ => Err(OpError::invalid_input( + "a model is keyed by exactly one of uuid or content_uuid", + )), + } + } + + async fn model_put(&mut self, put: ModelPut) -> OpResult { + let key = Self::model_key(put.uuid, put.content_uuid)?; + let store = self.store().await?; + store + .upsert(&put.model, &key, &put.data) + .await + .map_err(|e| OpError::failed(e.to_string()))?; + json(&serde_json::Value::Null) + } + + async fn model_get(&mut self, model: ModelRef) -> OpResult { + let key = Self::model_key(model.uuid, model.content_uuid)?; + let store = self.store().await?; + let rows = store + .facet_rows(&model.model, Some(&key), 1) + .await + .map_err(|e| OpError::failed(e.to_string()))?; + match rows.into_iter().next() { + Some(row) => json(&row), + None => Err(OpError::not_found()), + } + } + + async fn model_list(&mut self, list: ModelList) -> OpResult { + let store = self.store().await?; + let rows = store + .facet_rows( + &list.model, + None, + list.limit.unwrap_or(QUERY_CAP).min(QUERY_CAP), + ) + .await + .map_err(|e| OpError::failed(e.to_string()))?; + json(&rows) + } +} + +impl JobOps { + /// Inference, when the core has a provider for the model's category. + /// + /// It has none for the categories photos asks for (face detection, scene + /// classification, embeddings, language models), so a granted request + /// is answered `not_available` with the category named, and the job + /// decides what to do without it. The grant is still checked first: a + /// request for an undeclared category is refused, not deferred. + fn ai_infer(&self, payload: &[u8]) -> OpResult { + let (header, _input) = split_frame(payload)?; + let inference: Inference = parse(header)?; + let category = inference + .model + .split_once(':') + .map(|(category, _)| category) + .unwrap_or(&inference.model); + let granted = self + .manifest + .permissions + .use_models + .iter() + .any(|m| m.category == category); + if !granted { + return Err(OpError::permission_denied(format!( + "{} has no use_models grant for {category}", + self.extension_id + ))); + } + Err(OpError::not_available(format!( + "no {category} provider is installed for {} ({})", + inference.model, inference.task + ))) + } + + /// `config.json` beside the manifest, or `{}`. + async fn config_get(&self) -> OpResult { + match tokio::fs::read(self.plugin_path.join("config.json")).await { + Ok(bytes) => { + let value: serde_json::Value = serde_json::from_slice(&bytes) + .map_err(|e| OpError::failed(format!("config.json: {e}")))?; + json(&value) + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => json(&serde_json::json!({})), + Err(e) => Err(OpError::failed(format!("config.json: {e}"))), + } + } +} + +/// A framed payload: a little-endian u32 header length, the JSON header, +/// then raw bytes. +fn split_frame(payload: &[u8]) -> Result<(&[u8], &[u8]), OpError> { + let malformed = || OpError::invalid_input("malformed framed payload"); + let len = payload + .get(..4) + .and_then(|b| <[u8; 4]>::try_from(b).ok()) + .map(u32::from_le_bytes) + .ok_or_else(malformed)? as usize; + let rest = &payload[4..]; + if rest.len() < len { + return Err(malformed()); + } + Ok(rest.split_at(len)) +} + +fn parse Deserialize<'de>>(payload: &[u8]) -> Result { + serde_json::from_slice(payload).map_err(|e| OpError::invalid_input(e.to_string())) +} + +fn json(value: &T) -> OpResult { + serde_json::to_vec(value).map_err(|e| OpError::failed(e.to_string())) +} diff --git a/core/src/infra/extension/permissions.rs b/core/src/infra/extension/permissions.rs deleted file mode 100644 index 5f2f65493..000000000 --- a/core/src/infra/extension/permissions.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! Permission and security system for WASM extensions - -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use thiserror::Error; -use tokio::sync::RwLock; -use uuid::Uuid; - -use super::types::ManifestPermissions; - -#[derive(Error, Debug)] -pub enum PermissionError { - #[error("Extension not authorized: {0}")] - Unauthorized(String), - - #[error("Method not allowed: {0}")] - MethodNotAllowed(String), - - #[error("Library access denied: {0}")] - LibraryAccessDenied(String), - - #[error("Rate limit exceeded: {0}")] - RateLimitExceeded(String), -} - -/// Runtime permission checker with rate limiting -#[derive(Clone)] -pub struct ExtensionPermissions { - extension_id: String, - - /// Methods this extension can call (prefix matching) - allowed_methods: Vec, - - /// Libraries this extension can access ("*" or specific UUIDs) - allowed_libraries: Vec, - - /// Rate limiting state - rate_limiter: Arc>, - - /// Resource limits - pub max_memory_mb: usize, - pub max_concurrent_jobs: usize, -} - -struct RateLimiter { - requests_per_minute: usize, - recent_requests: Vec, -} - -impl ExtensionPermissions { - /// Create permissions from manifest - pub fn from_manifest(extension_id: String, manifest_perms: &ManifestPermissions) -> Self { - Self { - extension_id, - allowed_methods: manifest_perms.methods.clone(), - allowed_libraries: manifest_perms.libraries.clone(), - rate_limiter: Arc::new(RwLock::new(RateLimiter { - requests_per_minute: manifest_perms.rate_limits.requests_per_minute, - recent_requests: Vec::new(), - })), - max_memory_mb: manifest_perms.max_memory_mb, - max_concurrent_jobs: manifest_perms.rate_limits.concurrent_jobs, - } - } - - /// Check if extension can call this Wire method - pub fn can_call(&self, method: &str) -> bool { - // Check if any allowed prefix matches - self.allowed_methods - .iter() - .any(|prefix| method.starts_with(prefix)) - } - - /// Check if extension can access this library - pub fn can_access_library(&self, library_id: Uuid) -> bool { - // "*" means all libraries - if self.allowed_libraries.iter().any(|id| id == "*") { - return true; - } - - // Check if specific library UUID is allowed - self.allowed_libraries - .iter() - .any(|id| id.parse::().ok() == Some(library_id)) - } - - /// Check rate limit and record request - pub async fn check_rate_limit(&self) -> Result<(), PermissionError> { - let mut limiter = self.rate_limiter.write().await; - - let now = Instant::now(); - let one_minute_ago = now - Duration::from_secs(60); - - // Remove requests older than 1 minute - limiter - .recent_requests - .retain(|×tamp| timestamp > one_minute_ago); - - // Check if under limit - if limiter.recent_requests.len() >= limiter.requests_per_minute { - return Err(PermissionError::RateLimitExceeded(format!( - "Extension {} exceeded {} requests/minute", - self.extension_id, limiter.requests_per_minute - ))); - } - - // Record this request - limiter.recent_requests.push(now); - - Ok(()) - } - - /// Full permission check for a Wire operation - pub async fn authorize( - &self, - method: &str, - library_id: Option, - ) -> Result<(), PermissionError> { - // Check method permission - if !self.can_call(method) { - return Err(PermissionError::MethodNotAllowed(format!( - "Extension {} not allowed to call {}", - self.extension_id, method - ))); - } - - // Check library access if specified - if let Some(lib_id) = library_id { - if !self.can_access_library(lib_id) { - return Err(PermissionError::LibraryAccessDenied(format!( - "Extension {} cannot access library {}", - self.extension_id, lib_id - ))); - } - } - - // Check rate limit - self.check_rate_limit().await?; - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_method_permission() { - let perms = ExtensionPermissions { - extension_id: "test".to_string(), - allowed_methods: vec!["vdfs.".to_string(), "ai.ocr".to_string()], - allowed_libraries: vec!["*".to_string()], - rate_limiter: Arc::new(RwLock::new(RateLimiter { - requests_per_minute: 1000, - recent_requests: Vec::new(), - })), - max_memory_mb: 512, - max_concurrent_jobs: 10, - }; - - assert!(perms.can_call("vdfs.create_entry")); - assert!(perms.can_call("vdfs.write_sidecar")); - assert!(perms.can_call("ai.ocr")); - assert!(!perms.can_call("credentials.delete")); // Not allowed - } - - #[test] - fn test_library_permission() { - let lib_id = Uuid::new_v4(); - - let perms_all = ExtensionPermissions { - extension_id: "test".to_string(), - allowed_methods: vec![], - allowed_libraries: vec!["*".to_string()], - rate_limiter: Arc::new(RwLock::new(RateLimiter { - requests_per_minute: 1000, - recent_requests: Vec::new(), - })), - max_memory_mb: 512, - max_concurrent_jobs: 10, - }; - - assert!(perms_all.can_access_library(lib_id)); - - let perms_specific = ExtensionPermissions { - extension_id: "test".to_string(), - allowed_methods: vec![], - allowed_libraries: vec![lib_id.to_string()], - rate_limiter: Arc::new(RwLock::new(RateLimiter { - requests_per_minute: 1000, - recent_requests: Vec::new(), - })), - max_memory_mb: 512, - max_concurrent_jobs: 10, - }; - - assert!(perms_specific.can_access_library(lib_id)); - assert!(!perms_specific.can_access_library(Uuid::new_v4())); - } -} diff --git a/core/src/infra/extension/types.rs b/core/src/infra/extension/types.rs index f4210f05e..a41ade8d3 100644 --- a/core/src/infra/extension/types.rs +++ b/core/src/infra/extension/types.rs @@ -1,4 +1,4 @@ -//! Types for the WASM plugin system +//! The extension manifest and the loaded-plugin record. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -6,8 +6,12 @@ use std::path::PathBuf; use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex}; -/// Extension manifest (manifest.json) +/// Extension manifest (`manifest.json`). +/// +/// Unknown fields are refused rather than ignored, so a grant the host does +/// not understand fails at load instead of silently granting nothing. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct ExtensionManifest { pub id: String, pub name: String, @@ -18,86 +22,172 @@ pub struct ExtensionManifest { pub author: String, #[serde(default)] pub homepage: Option, + #[serde(default)] + pub min_core_version: Option, + #[serde(default)] + pub required_features: Vec, /// WASM file path (relative to manifest) pub wasm_file: PathBuf, - /// Permissions required by this extension + /// What the extension may read and write. A manifest that declares none + /// can still register and run jobs; every host operation that touches + /// library data checks here first. #[serde(default)] pub permissions: ManifestPermissions, + /// AI models the extension wants installed. Declaration only: no host + /// side downloads or serves them yet. + #[serde(default)] + pub models: Vec, + /// Configuration schema (JSON Schema) #[serde(default)] pub config_schema: Option, } -/// Permission declaration in manifest -#[derive(Debug, Clone, Serialize, Deserialize)] +/// Permission declaration in manifest. +/// +/// Sidecar kinds are per extension: `write_sidecars: ["faces"]` grants +/// `faces` under this extension's own namespace, never another's. An +/// extension may always read back the kinds it writes. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct ManifestPermissions { - /// Wire methods this extension can call (prefix matching) - /// e.g., ["vdfs.", "ai.ocr", "credentials.store"]. A manifest that - /// declares none can still register and run jobs. + /// Read records (and their bytes) from the library's source stores. #[serde(default)] - pub methods: Vec, - - /// Libraries this extension can access - /// "*" = all libraries, or specific UUIDs - #[serde(default = "default_all_libraries")] - pub libraries: Vec, - - /// Rate limits + pub read_records: Option, #[serde(default)] - pub rate_limits: RateLimits, - - /// Network access (for HTTP proxy) + pub read_sidecars: Vec, #[serde(default)] - pub network_access: Vec, - - /// Resource limits + pub write_sidecars: Vec, #[serde(default)] - pub max_memory_mb: usize, + pub write_tags: bool, + /// Namespaces this extension may write custom fields under. + #[serde(default)] + pub write_custom_fields: Vec, + #[serde(default)] + pub dispatch_jobs: bool, + #[serde(default)] + pub use_models: Vec, } -fn default_all_libraries() -> Vec { - vec!["*".to_string()] +impl ExtensionManifest { + /// What the host relies on beyond the shape serde enforces. + /// + /// The id becomes a directory name under the library (sidecars and the + /// model store), so it has to be one path-safe segment, and a glob the + /// grant parser cannot read would otherwise fail open as "every record". + pub fn validate(&self) -> Result<(), String> { + let id_safe = !self.id.is_empty() + && self.id != "." + && self.id != ".." + && self + .id + .chars() + .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')); + if !id_safe { + return Err(format!( + "extension id {:?} must be one segment of [A-Za-z0-9._-]", + self.id + )); + } + if let Some(glob) = self + .permissions + .read_records + .as_ref() + .and_then(|r| r.glob.as_deref()) + { + if glob_extensions(glob).is_none() { + return Err(format!( + "read_records glob {glob:?} is not understood; only a trailing `*.ext` or `*.{{a,b}}` is honored" + )); + } + } + Ok(()) + } +} + +impl ManifestPermissions { + pub fn can_read_sidecar(&self, kind: &str) -> bool { + self.read_sidecars.iter().any(|k| k == kind) || self.can_write_sidecar(kind) + } + + pub fn can_write_sidecar(&self, kind: &str) -> bool { + self.write_sidecars.iter().any(|k| k == kind) + } + + /// The extensions a glob grant names, lowercase; `None` when the grant is + /// unrestricted or absent. A glob the parser cannot read names nothing, + /// so a query under it answers no records rather than every record. + pub fn granted_extensions(&self) -> Option> { + self.read_records + .as_ref() + .and_then(|r| r.glob.as_deref()) + .map(|glob| glob_extensions(glob).unwrap_or_default()) + } + + /// Whether a record with this extension (lowercase, no dot) is readable. + pub fn can_read_record(&self, extension: Option<&str>) -> bool { + match &self.read_records { + None => false, + Some(ReadRecords { glob: None }) => true, + Some(ReadRecords { glob: Some(glob) }) => { + let Some(extension) = extension else { + return false; + }; + glob_extensions(glob) + .map(|allowed| allowed.iter().any(|e| e == extension)) + .unwrap_or(false) + } + } + } +} + +/// The extensions named by a glob of the form `**/*.{jpg,png}` or `*.jpg`. +/// +/// Only the extension list is honored today; the directory part of the glob +/// is accepted and ignored, since records are addressed by uuid rather than +/// by path when an extension asks for them. +fn glob_extensions(glob: &str) -> Option> { + let tail = glob.rsplit('/').next()?; + let tail = tail.strip_prefix("*.")?; + let list = tail + .strip_prefix('{') + .and_then(|t| t.strip_suffix('}')) + .unwrap_or(tail); + Some( + list.split(',') + .map(|e| e.trim().to_lowercase()) + .filter(|e| !e.is_empty()) + .collect(), + ) +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ReadRecords { + /// A glob over file names; `None` grants every record. + #[serde(default)] + pub glob: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RateLimits { - #[serde(default = "default_requests_per_minute")] - pub requests_per_minute: usize, - - #[serde(default = "default_concurrent_jobs")] - pub concurrent_jobs: usize, +#[serde(deny_unknown_fields)] +pub struct UseModel { + pub category: String, + #[serde(default)] + pub preference: Option, } -fn default_requests_per_minute() -> usize { - 1000 -} - -fn default_concurrent_jobs() -> usize { - 10 -} - -impl Default for RateLimits { - fn default() -> Self { - Self { - requests_per_minute: 1000, - concurrent_jobs: 10, - } - } -} - -impl Default for ManifestPermissions { - fn default() -> Self { - Self { - methods: vec![], - libraries: vec!["*".to_string()], - rate_limits: RateLimits::default(), - network_access: vec![], - max_memory_mb: 512, - } - } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ManifestModel { + pub name: String, + pub category: String, + pub source: serde_json::Value, + #[serde(default)] + pub description: String, } /// Loaded plugin instance @@ -106,7 +196,7 @@ pub struct LoadedPlugin { /// Directory under the plugin directory it was loaded from, which the /// manifest id need not match. pub dir_name: String, - pub manifest: ExtensionManifest, + pub manifest: Arc, pub loaded_at: DateTime, /// Set by the runtime when a guest call trapped; see `PluginRuntime`. pub poisoned: Arc, @@ -115,3 +205,70 @@ pub struct LoadedPlugin { /// Alias for consistency with other code pub type PluginManifest = ExtensionManifest; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn photos_manifest_permissions_parse_strictly() { + let manifest: ExtensionManifest = + serde_json::from_str(include_str!("../../../../extensions/photos/manifest.json")) + .unwrap(); + let perms = &manifest.permissions; + assert!(perms.can_read_record(Some("jpg"))); + assert!(!perms.can_read_record(Some("txt"))); + assert!(perms.can_write_sidecar("faces")); + assert!(perms.can_read_sidecar("faces")); + assert!(perms.can_read_sidecar("exif")); + assert!(!perms.can_write_sidecar("exif")); + assert_eq!(manifest.models.len(), 2); + } + + #[test] + fn an_unknown_permission_is_refused() { + let err = + serde_json::from_str::(r#"{"methods": ["query:"]}"#).unwrap_err(); + assert!(err.to_string().contains("unknown field")); + } + + #[test] + fn a_glob_the_parser_cannot_read_grants_nothing() { + let perms = ManifestPermissions { + read_records: Some(ReadRecords { + glob: Some("**/Photos/**".into()), + }), + ..Default::default() + }; + assert_eq!(perms.granted_extensions(), Some(Vec::new())); + assert!(!perms.can_read_record(Some("jpg"))); + } + + #[test] + fn the_loader_refuses_a_path_like_id_and_an_unreadable_glob() { + let mut manifest: ExtensionManifest = serde_json::from_str( + r#"{"id":"ok.ext","name":"x","version":"1","wasm_file":"x.wasm"}"#, + ) + .unwrap(); + assert!(manifest.validate().is_ok()); + manifest.id = "../escape".into(); + assert!(manifest.validate().is_err()); + manifest.id = "ok".into(); + manifest.permissions.read_records = Some(ReadRecords { + glob: Some("*".into()), + }); + assert!(manifest.validate().is_err()); + } + + #[test] + fn a_glob_without_a_list_names_one_extension() { + let perms = ManifestPermissions { + read_records: Some(ReadRecords { + glob: Some("*.PDF".into()), + }), + ..Default::default() + }; + assert!(perms.can_read_record(Some("pdf"))); + assert!(!perms.can_read_record(None)); + } +} diff --git a/core/src/infra/extension/wasm_job.rs b/core/src/infra/extension/wasm_job.rs index 89974d711..799249dbd 100644 --- a/core/src/infra/extension/wasm_job.rs +++ b/core/src/infra/extension/wasm_job.rs @@ -15,6 +15,7 @@ use tokio::sync::mpsc; use crate::infra::job::{error::JobError, prelude::*}; use super::host_functions::{JobBridge, JobEvent}; +use super::ops::JobOps; /// Generic job for executing WASM extension jobs #[derive(Debug, Serialize, Deserialize, Job)] @@ -50,8 +51,11 @@ impl crate::infra::job::traits::DynJob for WasmJob { } impl WasmJob { - async fn handle(&mut self, event: JobEvent, ctx: &JobContext<'_>) { + async fn handle(&mut self, event: JobEvent, ctx: &JobContext<'_>, ops: &mut JobOps) { match event { + JobEvent::Op { op, payload, reply } => { + let _ = reply.send(ops.handle(&op, &payload, ctx).await); + } JobEvent::Progress { fraction, message } => { ctx.progress(Progress::Generic(GenericProgress::new( fraction, @@ -101,14 +105,31 @@ impl JobHandler for WasmJob { .await .map_err(|e| JobError::ExecutionFailed(e.to_string()))?; } - let runtime = plugin_manager - .read() - .await - .runtime(&self.extension_id) - .await - .ok_or_else(|| { + let (runtime, manifest, plugin_path, models) = { + let pm = plugin_manager.read().await; + let not_loaded = || { JobError::ExecutionFailed(format!("Extension '{}' not loaded", self.extension_id)) - })?; + }; + ( + pm.runtime(&self.extension_id) + .await + .ok_or_else(not_loaded)?, + pm.get_manifest(&self.extension_id) + .await + .ok_or_else(not_loaded)?, + pm.plugin_path(&self.extension_id) + .await + .ok_or_else(not_loaded)?, + pm.model_registry(), + ) + }; + let mut ops = JobOps::new( + self.extension_id.clone(), + manifest, + ctx.library_arc(), + plugin_path, + models, + ); // A checkpoint outlives a kill; the job row only outlives a pause. if let Some(saved) = ctx.load_state::().await? { @@ -153,7 +174,7 @@ impl JobHandler for WasmJob { result = &mut guest => break result, event = events_rx.recv() => { if let Some(event) = event { - self.handle(event, &ctx).await; + self.handle(event, &ctx, &mut ops).await; } } _ = interrupt_poll.tick() => { @@ -164,8 +185,9 @@ impl JobHandler for WasmJob { } }; while let Ok(event) = events_rx.try_recv() { - self.handle(event, &ctx).await; + self.handle(event, &ctx, &mut ops).await; } + ops.finish().await; match exit { Ok(Ok(0)) => Ok(JobOutput::Success), diff --git a/core/src/lib.rs b/core/src/lib.rs index 0e52ad81d..8f2185fac 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -186,11 +186,7 @@ impl Core { warn!("Could not create extensions directory: {e}"); } - let mut pm = crate::infra::extension::PluginManager::new( - plugin_dir, - context.clone(), - Arc::new(api_dispatcher.clone()), - ); + let mut pm = crate::infra::extension::PluginManager::new(plugin_dir); let loaded = pm.load_all().await; info!("Loaded {} extension(s): {:?}", loaded.len(), loaded); diff --git a/core/src/ops/extensions/list.rs b/core/src/ops/extensions/list.rs index 568ef8826..b611cbaa2 100644 --- a/core/src/ops/extensions/list.rs +++ b/core/src/ops/extensions/list.rs @@ -82,8 +82,8 @@ impl CoreQuery for ListExtensionsQuery { jobs.sort_by(|a, b| a.name.cmp(&b.name)); extensions.push(ExtensionInfo { id, - name: manifest.name, - version: manifest.version, + name: manifest.name.clone(), + version: manifest.version.clone(), jobs, }); } diff --git a/core/tests/wasm_extension_test.rs b/core/tests/wasm_extension_test.rs index 41ad84dc9..22b1d008d 100644 --- a/core/tests/wasm_extension_test.rs +++ b/core/tests/wasm_extension_test.rs @@ -19,7 +19,10 @@ use sd_core::{ action::LibraryAction, job::{database::checkpoint, types::JobStatus}, }, - ops::extensions::{RunExtensionJobAction, RunExtensionJobInput}, + ops::{ + extensions::{RunExtensionJobAction, RunExtensionJobInput}, + sources::track::{TrackSourceAction, TrackSourceInput}, + }, Core, }; use sea_orm::EntityTrait; @@ -35,18 +38,137 @@ struct CounterState { processed: Vec, } -fn install_test_extension(data_dir: &Path) { +fn install_extension(data_dir: &Path, dir: &str, wasm: &str) { let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() - .join("extensions/test-extension"); - let target = data_dir.join("extensions/test-extension"); + .join("extensions") + .join(dir); + let target = data_dir.join("extensions").join(dir); std::fs::create_dir_all(&target).unwrap(); - for file in ["manifest.json", "test_extension.wasm"] { + for file in ["manifest.json", wasm] { std::fs::copy(source.join(file), target.join(file)).unwrap(); } } +fn install_test_extension(data_dir: &Path) { + install_extension(data_dir, "test-extension", "test_extension.wasm"); +} + +/// Start an extension job and wait for it to end, whichever way. +async fn run_to_end( + core: &Core, + library: &Arc, + job: &str, + state: serde_json::Value, +) -> sd_core::infra::job::JobInfo { + let started = RunExtensionJobAction::from_input(RunExtensionJobInput { + job: job.into(), + state: Some(state), + }) + .unwrap() + .execute(library.clone(), core.context.clone()) + .await + .unwrap(); + let deadline = tokio::time::Instant::now() + Duration::from_secs(60); + loop { + let info = library + .jobs() + .get_job_info(started.job_id) + .await + .unwrap() + .unwrap(); + if matches!( + info.status, + JobStatus::Completed | JobStatus::Failed | JobStatus::Cancelled + ) { + return info; + } + assert!( + tokio::time::Instant::now() < deadline, + "job {job} never ended" + ); + tokio::time::sleep(Duration::from_millis(20)).await; + } +} + +/// A library with one tracked source holding a dozen JPEGs (distinct bytes, +/// JPEG magic first) and two text files, hashed through, so every record +/// has a content identity. +async fn fixture_library( + core: &Core, + root: &Path, +) -> (Arc, Vec) { + let library = core + .libraries + .create_library("Photos", None, core.context.clone()) + .await + .unwrap(); + let source_dir = root.join("photos"); + std::fs::create_dir_all(source_dir.join("trip")).unwrap(); + for i in 0..12u8 { + let mut bytes = vec![0xFF, 0xD8, 0xFF, 0xE0, i]; + bytes.extend(std::iter::repeat_n(i, 1000 + i as usize * 7)); + let dir = if i % 2 == 0 { "" } else { "trip/" }; + std::fs::write(source_dir.join(format!("{dir}IMG_{i:04}.JPG")), bytes).unwrap(); + } + std::fs::write(source_dir.join("notes.txt"), b"not a photo").unwrap(); + std::fs::write(source_dir.join("trip/itinerary.txt"), b"day one").unwrap(); + + let tracked = TrackSourceAction::from_input(TrackSourceInput { + path: source_dir, + name: None, + unfiltered: false, + }) + .unwrap() + .execute(library.clone(), core.context.clone()) + .await + .unwrap(); + let store = core + .context + .volume_index() + .store_for(&tracked.root) + .await + .expect("the tracked source has a store"); + let deadline = tokio::time::Instant::now() + Duration::from_secs(60); + loop { + let contents = store.counts().await.map_or(0, |counts| counts.contents); + if contents == 14 { + break; + } + assert!( + tokio::time::Instant::now() < deadline, + "{contents} of 14 files were hashed" + ); + tokio::time::sleep(Duration::from_millis(50)).await; + } + let mut files = sd_store::read::files_beneath( + store.db().pool(), + "", + sd_store::read::Start::First, + None, + false, + 100, + ) + .await + .unwrap(); + files.sort_by(|a, b| a.name.cmp(&b.name)); + assert_eq!(files.len(), 14); + (library, files) +} + +fn digest_sidecar(library: &sd_core::library::Library, content_uuid: Uuid) -> PathBuf { + library + .path() + .join("sidecars") + .join(sd_sidecar_path::relative_path( + &content_uuid, + &sd_sidecar_path::extension_kind_directory("test-extension", "digest"), + "default", + "json", + )) +} + async fn wait_for_status( library: &sd_core::library::Library, job_id: Uuid, @@ -158,7 +280,9 @@ async fn daemon_discovers_the_extension_and_runs_its_job() { /// A paused extension job survives a daemon restart and continues from the /// state the guest last checkpointed. -/// Collects every log line a guest emits through `spacedrive_log`. +/// Collects every log line a guest emits through `spacedrive_log`, and every +/// line the job context logs on the guest's behalf (task attempts, host +/// operations). struct GuestLog(Arc>); impl tracing_subscriber::Layer for GuestLog { @@ -167,7 +291,8 @@ impl tracing_subscriber::Layer for GuestLog { event: &tracing::Event<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>, ) { - if !event.metadata().target().ends_with("host_functions") { + let target = event.metadata().target(); + if !target.ends_with("host_functions") && !target.ends_with("job::context") { return; } let mut line = String::new(); @@ -292,26 +417,142 @@ async fn extension_job_resumes_after_restart() { core.shutdown().await.unwrap(); } -/// The photos extension (`extensions/photos`, fixture `photos.wasm`) loads -/// beside the test extension and registers its jobs. Its jobs stop at the -/// first SDK call with no host function behind it; the guest's panic reaches -/// the host log and the job fails instead of hanging. +/// The test extension's `catalog` job reads records and their bytes through +/// the library's source stores, writes one sidecar per content and one model +/// per file into the extension's own store, and leaves digested files alone +/// on a second run. #[tokio::test(flavor = "multi_thread")] -async fn photos_extension_loads_and_stops_at_the_first_missing_host_function() { +async fn extension_job_reads_records_and_writes_sidecars_and_models() { let guest_log = guest_log(); let temp_dir = TempDir::new().unwrap(); - install_test_extension(temp_dir.path()); - let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .join("extensions/photos"); - let target = temp_dir.path().join("extensions/photos"); - std::fs::create_dir_all(&target).unwrap(); - for file in ["manifest.json", "photos.wasm"] { - std::fs::copy(source.join(file), target.join(file)).unwrap(); + install_test_extension(temp_dir.path().join("core").as_path()); + let core = Core::new(temp_dir.path().join("core")).await.unwrap(); + let (library, files) = fixture_library(&core, temp_dir.path()).await; + let jpegs: Vec<&sd_store::FsEntry> = files + .iter() + .filter(|f| f.extension.as_deref() == Some("JPG")) + .collect(); + assert_eq!(jpegs.len(), 12); + + guest_log.lock().unwrap().clear(); + let info = run_to_end( + &core, + &library, + "test-extension:catalog", + serde_json::json!({ "extensions": ["jpg"] }), + ) + .await; + assert_eq!( + info.status, + JobStatus::Completed, + "{:?}", + info.error_message + ); + + // One digest sidecar per photo, holding what the guest read + for jpeg in &jpegs { + let content_uuid = jpeg.content_uuid.expect("hashed"); + let path = digest_sidecar(&library, content_uuid); + let digest: serde_json::Value = + serde_json::from_slice(&std::fs::read(&path).unwrap_or_else(|e| { + panic!("digest sidecar for {}: {e} ({})", jpeg.name, path.display()) + })) + .unwrap(); + assert_eq!(digest["size"], jpeg.size.unwrap()); + assert_eq!( + digest["first_bytes"], + serde_json::json!([0xFF, 0xD8, 0xFF, 0xE0]) + ); + assert_eq!(digest["record_name"], jpeg.name); + } + // The text files were outside the query and got no sidecar + for other in files + .iter() + .filter(|f| f.extension.as_deref() == Some("txt")) + { + assert!(!digest_sidecar(&library, other.content_uuid.unwrap()).exists()); } - let core = Core::new(temp_dir.path().to_path_buf()).await.unwrap(); + // One CatalogEntry model per photo in the extension's store, read back + // with its field types: the update after create landed too + let store = sd_store::SourceManager::open_file_read_only( + &library.path().join("extensions/test-extension/data.db"), + ) + .await + .unwrap(); + let mut entries = store.facet_rows("CatalogEntry", None, 100).await.unwrap(); + entries.sort_by_key(|e| e["name"].as_str().unwrap().to_string()); + assert_eq!(entries.len(), 12); + for (entry, jpeg) in entries.iter().zip(&jpegs) { + assert_eq!(entry["name"], jpeg.name); + assert_eq!(entry["record"], jpeg.uuid.to_string()); + assert_eq!(entry["size"], jpeg.size.unwrap()); + assert_eq!(entry["seen_twice"], true); + assert_eq!( + entry["first_bytes"], + serde_json::json!([0xFF, 0xD8, 0xFF, 0xE0]) + ); + Uuid::parse_str(entry["id"].as_str().unwrap()).expect("a v4 uuid from host entropy"); + chrono::DateTime::parse_from_rfc3339(entry["catalogued_at"].as_str().unwrap()) + .expect("a timestamp from the host clock"); + } + let log = guest_log.lock().unwrap().clone(); + assert!( + log.contains("Catalog holds 12 entries, 12 digested this run, 0 skipped"), + "{log}" + ); + + // A second run finds every photo digested and writes nothing new + guest_log.lock().unwrap().clear(); + let info = run_to_end( + &core, + &library, + "test-extension:catalog", + serde_json::json!({ "extensions": ["jpg"] }), + ) + .await; + assert_eq!( + info.status, + JobStatus::Completed, + "{:?}", + info.error_message + ); + let log = guest_log.lock().unwrap().clone(); + assert_eq!(log.matches("already digested").count(), 12, "{log}"); + assert!( + log.contains("Catalog holds 12 entries, 0 digested this run, 12 skipped"), + "{log}" + ); + assert_eq!( + store + .facet_rows("CatalogEntry", None, 100) + .await + .unwrap() + .len(), + 12 + ); + store.pool().close().await; + + core.shutdown().await.unwrap(); +} + +/// The photos extension's `analyze_photos` runs end to end over the fixture: +/// every photo is read through its record, the host answers face detection +/// with not_available, the job warns once and completes with no faces +/// sidecar, so a later run with a detector picks the photos up. A record +/// outside the manifest's read_records glob is refused. +#[tokio::test(flavor = "multi_thread")] +async fn photos_analyze_photos_runs_end_to_end_without_a_detector() { + let guest_log = guest_log(); + let temp_dir = TempDir::new().unwrap(); + install_test_extension(temp_dir.path().join("core").as_path()); + install_extension( + temp_dir.path().join("core").as_path(), + "photos", + "photos.wasm", + ); + + let core = Core::new(temp_dir.path().join("core")).await.unwrap(); let pm = core.plugin_manager.as_ref().expect("plugin manager"); let mut loaded = pm.read().await.list_plugins().await; loaded.sort(); @@ -333,64 +574,100 @@ async fn photos_extension_loads_and_stops_at_the_first_missing_host_function() { "identify_places" ] ); + let models = pm.read().await.model_registry(); + let schema = models + .schema_for("com.spacedrive.photos") + .expect("photos declared its models"); + let mut declared: Vec<&String> = schema.models.keys().collect(); + declared.sort(); + assert_eq!(declared, ["Album", "Moment", "Person", "Photo", "Place"]); - // Twice: the second run lands on a fresh instance, since the first one - // aborted mid-call. - let library = core.libraries.list().await.into_iter().next().unwrap(); - for _ in 0..2 { - guest_log.lock().unwrap().clear(); - let started = RunExtensionJobAction::from_input(RunExtensionJobInput { - job: "com.spacedrive.photos:analyze_photos".into(), - state: None, - }) - .unwrap() - .execute(library.clone(), core.context.clone()) - .await - .unwrap(); + let (library, files) = fixture_library(&core, temp_dir.path()).await; + let photo_ids: Vec = files + .iter() + .filter(|f| f.extension.as_deref() == Some("JPG")) + .map(|f| f.uuid) + .collect(); - let deadline = tokio::time::Instant::now() + Duration::from_secs(30); - let info = loop { - let info = library - .jobs() - .get_job_info(started.job_id) - .await - .unwrap() - .unwrap(); - if info.status == JobStatus::Failed { - break info; - } - assert_ne!( - info.status, - JobStatus::Completed, - "nothing backs this job yet" - ); - assert!( - tokio::time::Instant::now() < deadline, - "the job never ended" - ); - tokio::time::sleep(Duration::from_millis(20)).await; - }; - assert!( - info.error_message - .as_deref() - .unwrap_or("") - .contains("WASM trap"), - "{:?}", - info.error_message - ); - let guest_log = guest_log.lock().unwrap().clone(); - assert!( - guest_log.contains("guest panic:"), - "the guest's panic should reach the host log:\n{guest_log}" - ); - } + guest_log.lock().unwrap().clear(); + let info = run_to_end( + &core, + &library, + "com.spacedrive.photos:analyze_photos", + serde_json::json!({ "photo_ids": photo_ids }), + ) + .await; assert_eq!( - registry - .list_jobs_for_extension("com.spacedrive.photos") - .len(), - 4, - "the reloaded extension registers its jobs again" + info.status, + JobStatus::Completed, + "{:?}", + info.error_message ); + let log = guest_log.lock().unwrap().clone(); + assert!( + log.contains("Job analyze_photos_batch completed successfully"), + "{log}" + ); + assert!(!log.contains("guest panic"), "{log}"); + // No detector, so no faces sidecar anywhere under photos' namespace + let photos_sidecars = walk(&library.path().join("sidecars")) + .into_iter() + .filter(|p| p.to_string_lossy().contains("com.spacedrive.photos")) + .count(); + assert_eq!(photos_sidecars, 0); + + // The host logged each task attempt, and the detector's absence was the + // reason the detection task failed; the policy did not retry a refusal + assert_eq!( + log.matches("task detect_faces_in_photo attempt 1/3 started") + .count(), + 12, + "{log}" + ); + assert!( + log.contains("attempt 1 failed after") + && log.contains("no face_detection provider is installed"), + "{log}" + ); + assert!(!log.contains("attempt 2/3"), "{log}"); + assert!( + log.contains("task cluster_faces_into_people attempt 1/2 started") + && log.contains("task generate_face_tags attempt 1/1 started"), + "{log}" + ); + + // A text file is outside photos' read_records glob + let text = files + .iter() + .find(|f| f.extension.as_deref() == Some("txt")) + .unwrap(); + guest_log.lock().unwrap().clear(); + let info = run_to_end( + &core, + &library, + "com.spacedrive.photos:analyze_photos", + serde_json::json!({ "photo_ids": [text.uuid] }), + ) + .await; + assert_eq!(info.status, JobStatus::Failed); + let log = guest_log.lock().unwrap().clone(); + assert!(log.contains("Permission denied"), "{log}"); core.shutdown().await.unwrap(); } + +fn walk(dir: &Path) -> Vec { + let mut out = Vec::new(); + let Ok(entries) = std::fs::read_dir(dir) else { + return out; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + out.extend(walk(&path)); + } else { + out.push(path); + } + } + out +} diff --git a/crates/sdk-macros/src/extension.rs b/crates/sdk-macros/src/extension.rs index b80a71ac9..4b25cb0a7 100644 --- a/crates/sdk-macros/src/extension.rs +++ b/crates/sdk-macros/src/extension.rs @@ -12,6 +12,8 @@ struct ExtensionArgs { name: String, version: String, jobs: Vec, + /// Model types whose facets `plugin_init` declares to the host. + models: Vec, // We'll ignore other parameters for now (description, permissions, etc.) // They can be used by tooling but don't need codegen } @@ -22,6 +24,7 @@ impl Parse for ExtensionArgs { let mut name = None; let mut version = None; let mut jobs = Vec::new(); + let mut models = Vec::new(); while !input.is_empty() { let ident: Ident = input.parse()?; @@ -50,6 +53,16 @@ impl Parse for ExtensionArgs { } } } + "models" => { + let content; + syn::bracketed!(content in input); + while !content.is_empty() { + models.push(content.parse()?); + if content.peek(Token![,]) { + content.parse::()?; + } + } + } // Ignore other parameters - just skip their values "description" | "min_core_version" => { let _: LitStr = input.parse()?; @@ -95,6 +108,7 @@ impl Parse for ExtensionArgs { name: name.ok_or_else(|| input.error("missing name parameter"))?, version: version.ok_or_else(|| input.error("missing version parameter"))?, jobs, + models, }) } } @@ -127,6 +141,21 @@ pub fn extension_impl(args: TokenStream, input: TokenStream) -> TokenStream { } }); + let model_registrations = args.models.iter().map(|model| { + quote! { + if let Err(e) = ::spacedrive_sdk::ffi::register_model_with_host( + <#model as ::spacedrive_sdk::models::ExtensionModel>::DEFINITION, + ) { + ::spacedrive_sdk::ffi::log_error(&format!( + "Failed to register model {}: {}", + <#model as ::spacedrive_sdk::models::ExtensionModel>::MODEL_TYPE, + e + )); + return 1; + } + } + }); + let expanded = quote! { #input_struct @@ -148,6 +177,9 @@ pub fn extension_impl(args: TokenStream, input: TokenStream) -> TokenStream { // Register all jobs #(#job_registrations)* + // Declare data models, so the host has their facets before a job runs + #(#model_registrations)* + ::spacedrive_sdk::ffi::log_info(&format!( "✓ {} v{} initialized!", #ext_name, diff --git a/crates/sdk-macros/src/model.rs b/crates/sdk-macros/src/model.rs index 1e0f9b718..67e0210d0 100644 --- a/crates/sdk-macros/src/model.rs +++ b/crates/sdk-macros/src/model.rs @@ -1,9 +1,18 @@ //! Model macro implementation - generates ExtensionModel trait impl -//! Handles field attributes: #[entry], #[sidecar], #[custom_field], #[computed], etc. +//! +//! The struct's fields become the model's facet columns in the extension's +//! store. Column types follow the Rust types: strings and uuids are `string`, +//! integers `integer`, floats `float`, bools `boolean`, `DateTime` is +//! `datetime`, `Option` is `T`, and anything else is stored as `json`. +//! +//! The field attributes `#[sidecar]`, `#[sync]`, `#[computed]`, +//! `#[vectorized]`, `#[entry]`, `#[metadata]`, `#[custom_field]` and +//! `#[user_metadata]` are accepted and stripped; nothing acts on them yet, so +//! every field is a column. use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, Attribute, Data, DeriveInput, Fields}; +use syn::{parse_macro_input, Attribute, Data, DeriveInput, Fields, PathArguments, Type}; pub fn model_impl(_args: TokenStream, input: TokenStream) -> TokenStream { let mut input = parse_macro_input!(input as DeriveInput); @@ -11,6 +20,7 @@ pub fn model_impl(_args: TokenStream, input: TokenStream) -> TokenStream { // Find the id/uuid field before modifying let uuid_field = find_uuid_field(&input); let name = input.ident.clone(); + let definition = definition_json(&input); // Strip known field attributes (they'll be processed later when macros are enhanced) strip_field_attributes(&mut input); @@ -20,6 +30,7 @@ pub fn model_impl(_args: TokenStream, input: TokenStream) -> TokenStream { impl ::spacedrive_sdk::models::ExtensionModel for #name { const MODEL_TYPE: &'static str = stringify!(#name); + const DEFINITION: &'static str = #definition; fn uuid(&self) -> ::spacedrive_sdk::types::Uuid { self.#uuid_field @@ -34,6 +45,54 @@ pub fn model_impl(_args: TokenStream, input: TokenStream) -> TokenStream { TokenStream::from(expanded) } +/// The model definition the host registers: `{"name":..,"fields":{..}}`, +/// with fields in declaration order. +fn definition_json(input: &DeriveInput) -> String { + let mut fields = Vec::new(); + if let Data::Struct(data_struct) = &input.data { + if let Fields::Named(named) = &data_struct.fields { + for field in &named.named { + if let Some(ident) = &field.ident { + fields.push(format!("\"{}\":\"{}\"", ident, field_type_name(&field.ty))); + } + } + } + } + format!( + "{{\"name\":\"{}\",\"fields\":{{{}}}}}", + input.ident, + fields.join(",") + ) +} + +fn field_type_name(ty: &Type) -> &'static str { + let Type::Path(path) = ty else { + return "json"; + }; + let Some(last) = path.path.segments.last() else { + return "json"; + }; + let ident = last.ident.to_string(); + if ident == "Option" { + if let PathArguments::AngleBracketed(generics) = &last.arguments { + if let Some(syn::GenericArgument::Type(inner)) = generics.args.first() { + return field_type_name(inner); + } + } + return "json"; + } + match ident.as_str() { + "String" | "str" | "Uuid" => "string", + "i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64" | "usize" => { + "integer" + } + "f32" | "f64" => "float", + "bool" => "boolean", + "DateTime" | "NaiveDateTime" => "datetime", + _ => "json", + } +} + fn find_uuid_field(input: &DeriveInput) -> syn::Ident { if let Data::Struct(data_struct) = &input.data { if let Fields::Named(fields) = &data_struct.fields { @@ -54,7 +113,6 @@ fn strip_field_attributes(input: &mut DeriveInput) { if let Data::Struct(ref mut data_struct) = input.data { if let Fields::Named(ref mut fields) = data_struct.fields { for field in &mut fields.named { - // Remove known field attributes (for now - will be processed in future) field.attrs.retain(|attr| !is_model_field_attribute(attr)); } } diff --git a/crates/sdk-macros/src/task.rs b/crates/sdk-macros/src/task.rs index 3227f11e8..b181e67e1 100644 --- a/crates/sdk-macros/src/task.rs +++ b/crates/sdk-macros/src/task.rs @@ -1,17 +1,130 @@ //! Task macro implementation +//! +//! `#[task(retries = 2, timeout_ms = 30000)]` on an async function makes a +//! unit struct of the function's name that implements `spacedrive_sdk::Task`, +//! so `ctx.run(detect_faces, photo)` passes the policy along with the code. +//! The body keeps running as a renamed function the trait impl calls. use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, ItemFn}; +use quote::{format_ident, quote}; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, FnArg, GenericArgument, ItemFn, Lit, PathArguments, ReturnType, Token, Type, +}; -pub fn task_impl(_args: TokenStream, input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemFn); +struct TaskArgs { + retries: u32, + timeout_ms: u64, +} + +impl Parse for TaskArgs { + fn parse(input: ParseStream) -> syn::Result { + let mut args = TaskArgs { + retries: 0, + timeout_ms: 0, + }; + while !input.is_empty() { + let key: syn::Ident = input.parse()?; + input.parse::()?; + let value: Lit = input.parse()?; + match (key.to_string().as_str(), value) { + ("retries", Lit::Int(n)) => args.retries = n.base10_parse()?, + ("timeout_ms", Lit::Int(n)) => args.timeout_ms = n.base10_parse()?, + // Capability routing has no host side; the attribute is kept + // for documentation. + ("requires_capability", Lit::Str(_)) => {} + (other, _) => { + return Err(syn::Error::new( + key.span(), + format!("unknown task attribute `{other}`"), + )) + } + } + if input.peek(Token![,]) { + input.parse::()?; + } + } + Ok(args) + } +} + +pub fn task_impl(args: TokenStream, input: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as TaskArgs); + let mut input_fn = parse_macro_input!(input as ItemFn); + + let name = input_fn.sig.ident.clone(); + let name_str = name.to_string(); + let vis = input_fn.vis.clone(); + let docs: Vec<_> = input_fn + .attrs + .iter() + .filter(|attr| attr.path().is_ident("doc")) + .cloned() + .collect(); + let inner = format_ident!("__task_{}", name); + input_fn.sig.ident = inner.clone(); + + let args_type = match input_fn.sig.inputs.iter().nth(1) { + Some(FnArg::Typed(pat)) => (*pat.ty).clone(), + _ => { + return syn::Error::new_spanned( + &input_fn.sig, + "a task takes (ctx: TaskContext, args: A)", + ) + .to_compile_error() + .into() + } + }; + let output_type = match &input_fn.sig.output { + ReturnType::Type(_, ty) => result_ok_type(ty), + ReturnType::Default => None, + }; + let Some(output_type) = output_type else { + return syn::Error::new_spanned(&input_fn.sig.output, "a task returns TaskResult") + .to_compile_error() + .into(); + }; + let retries = args.retries; + let timeout_ms = args.timeout_ms; - // For now, just pass through - // Future: Generate task wrapper with retry logic, timeout, etc. let expanded = quote! { - #input + #input_fn + + #(#docs)* + #[allow(non_camel_case_types)] + #[derive(Clone, Copy)] + #vis struct #name; + + impl ::spacedrive_sdk::tasks::Task for #name { + type Args = #args_type; + type Output = #output_type; + const NAME: &'static str = #name_str; + const RETRIES: u32 = #retries; + const TIMEOUT_MS: u64 = #timeout_ms; + + async fn call( + ctx: ::spacedrive_sdk::tasks::TaskContext, + args: Self::Args, + ) -> ::spacedrive_sdk::tasks::TaskResult { + #inner(ctx, args).await + } + } }; TokenStream::from(expanded) } + +/// `T` out of `TaskResult` or `Result` or `Result`. +fn result_ok_type(ty: &Type) -> Option { + let Type::Path(path) = ty else { + return None; + }; + let last = path.path.segments.last()?; + let PathArguments::AngleBracketed(generics) = &last.arguments else { + return None; + }; + match generics.args.first()? { + GenericArgument::Type(ok) => Some(ok.clone()), + _ => None, + } +} diff --git a/crates/sdk/.cargo/config.toml b/crates/sdk/.cargo/config.toml new file mode 100644 index 000000000..0dc0dbca0 --- /dev/null +++ b/crates/sdk/.cargo/config.toml @@ -0,0 +1,4 @@ +# The SDK supplies getrandom's custom backend from the host; a guest that +# links it builds with the same cfg (see each extension's .cargo/config.toml). +[target.wasm32-unknown-unknown] +rustflags = ["--cfg", "getrandom_backend=\"custom\""] diff --git a/crates/sdk/API_REFERENCE.md b/crates/sdk/API_REFERENCE.md index bf49398db..bd8ff29cf 100644 --- a/crates/sdk/API_REFERENCE.md +++ b/crates/sdk/API_REFERENCE.md @@ -1,13 +1,30 @@ # Spacedrive SDK API Reference -**Status:** Stubs for type-checking (implementations are `todo!()`) -**Purpose:** Full API surface from `docs/sdk/sdk.md` - allows extensions to compile +**Status:** Records, sidecars, models, tasks, entropy, clock and config have a host side behind `spacedrive_op`; inference answers `Error::NotAvailable`; tags, custom fields, agents and actions remain stubs. +**Purpose:** The API surface extensions compile against, and what each part does at runtime. --- ## Overview -The SDK now includes **all APIs** documented in the specification as type-checked stubs. +Every method that returns data from the host goes through one import, +`spacedrive_op(name, payload)`. The host checks the manifest's `permissions` +block and answers JSON, or an error with a stable code the SDK maps onto +`Error` (`NotFound`, `PermissionDenied`, `InvalidInput`, `NotAvailable`, +`OperationFailed`). + +### Renames since the stub phase + +| Was | Now | Why | +|-----|-----|-----| +| `Entry`, `EntryKind` | `Record`, `RecordKind` | The entry table is gone; files are records in a source store. | +| `vdfs().get_entry(uuid)` | `vdfs().get_record(uuid)` | | +| `vdfs().query_entries()` | `vdfs().query_records()` | Filters by source, path, extension and type. `with_tag` and `where_metadata` are refused by the host. | +| new | `vdfs().read_record(uuid)`, `Record::read()` | The bytes of a record, read from its source. | +| `save_sidecar(content_uuid, kind, extension_id, data)` | `save_sidecar(content_uuid, kind, data)` | The host keys the sidecar by the calling extension. | +| `add_tag(metadata_id, tag)` | `add_tag(record_uuid, tag)` | Tags attach to records; no host side yet. | +| new | `spacedrive_sdk::clock::now()` | Wall-clock time; `SystemTime::now` panics on wasm32-unknown-unknown. | +| new | `#[model]` emits a definition the host registers | Models live in an extension-owned store under `/extensions//`. | ### Modules @@ -38,9 +55,9 @@ pub type QueryResult = std::result::Result; pub type TaskResult = std::result::Result; // Core entities -pub struct Entry { id, uuid, name, kind, ... } +pub struct Record { uuid, source, path, name, kind, extension, size, content_uuid, ... } pub struct Tag { id, name, color, icon } -pub enum EntryKind { File, Directory, Symlink, Virtual } +pub enum RecordKind { File, Directory, Symlink } pub enum Priority { Low, Normal, High } pub enum Capability { GPU, CPU } pub enum Progress { Indeterminate, Simple, Complete } @@ -56,9 +73,15 @@ pub struct Pdf; ```rust impl VdfsContext { - // Entry queries - fn query_entries() -> EntryQuery - async fn get_entry(uuid) -> Result + // Record queries (read_records grant) + fn query_records() -> RecordQuery + async fn get_record(uuid) -> Result + async fn read_record(uuid) -> Result> + + // Sidecars (read_sidecars / write_sidecars grants), one JSON document per kind + fn sidecar_exists(content_uuid, kind) -> Result + async fn read_sidecar(content_uuid, kind) -> Result + async fn write_sidecar(content_uuid, kind, data) -> Result<()> // Model operations (content-scoped) async fn create_model_for_content(content_uuid, model) -> Result<()> @@ -70,29 +93,29 @@ impl VdfsContext { async fn get_model(uuid) -> Result fn query_models() -> ModelQuery - // Tagging + // Tagging (no host side yet, returns Error::NotAvailable) async fn add_tag_to_content(content_uuid, tag) -> Result<()> async fn add_tag_to_model(model_uuid, tag) -> Result<()> - async fn add_tag(metadata_id, tag) -> Result<()> + async fn add_tag(record_uuid, tag) -> Result<()> - // Custom fields - async fn update_custom_field(entry_uuid, field, value) -> Result<()> + // Custom fields (no host side yet) + async fn update_custom_field(record_uuid, field, value) -> Result<()> // Permissions fn in_granted_scope(path) -> bool } -// Entry query builder -impl EntryQuery { +// Record query builder +impl RecordQuery { + fn in_source(source_uuid) -> Self fn in_location(path) -> Self + fn with_extensions(extensions) -> Self fn of_type() -> Self - fn where_content_id(content_uuid) -> Self - fn on_this_device() -> Self - fn with_tag(tag) -> Self - fn with_sidecar(kind) -> Self - async fn first() -> Result> - async fn collect() -> Result> - fn map(f) -> MappedQuery + fn with_tag(tag) -> Self // refused by the host today + fn where_metadata(field, predicate) -> Self // refused by the host today + fn limit(n) -> Self + async fn first() -> Result> + async fn collect() -> Result> } // Model query builder @@ -125,7 +148,7 @@ impl ModelHandle { fn prompt_template(template_name: &str) -> PromptBuilder async fn detect_faces(image_data: &[u8]) -> Result> async fn classify(image_data: &[u8]) -> Result> - async fn ocr_document(entry: &Entry) -> Result + async fn ocr_document(record: &Record) -> Result async fn embed_text(text: &str) -> Result> } @@ -244,7 +267,8 @@ impl JobContext { fn progress(progress: Progress) async fn check_interrupt() -> Result<()> // Async version fn sidecar_exists(content_uuid, kind) -> Result - async fn save_sidecar(content_uuid, kind, extension_id, data) -> Result<()> + async fn save_sidecar(content_uuid, kind, data) -> Result<()> + async fn read_sidecar(content_uuid, kind) -> Result fn memory() -> MemoryHandle<()> fn config() -> &C fn notify() -> NotificationBuilder @@ -286,7 +310,7 @@ pub enum Change { UpdateCustomField { entry_id, field, value }, AddTag { target, tag }, CreateDirectory { name, parent }, - MoveEntry { entry, destination }, + MoveRecord { record, destination }, } pub struct ExecutionResult { @@ -354,36 +378,30 @@ All macros are currently pass-through stubs: ## Usage Examples -### Photos Extension (Now Type-Checks!) +### Photos Extension + +`extensions/photos/src/jobs/analyze.rs` is the worked example: it reads each +photo through `get_record` and `Record::read`, runs face detection as a +`#[task]` with a retry policy, handles `Error::NotAvailable` with one warning +and a skip, and writes a `faces` sidecar when a detector answers. ```rust -#[model(scope = "content")] -struct PhotoAnalysis { - detected_faces: Vec, - // ... compiles! -} - -#[job] -async fn analyze_photos(ctx: &JobContext, content_uuids: Vec) -> JobResult<()> { - for content_uuid in content_uuids { - let entry = ctx.vdfs() // Type-checks - .query_entries() - .where_content_id(content_uuid) - .first() - .await?; - - let faces = ctx.ai() // Type-checks - .from_registered("face_detection") - .detect_faces(&image_data) - .await?; - - ctx.vdfs().create_model_for_content(content_uuid, analysis).await?; // Type-checks +#[job(name = "analyze_photos_batch")] +async fn analyze_photos(ctx: &JobContext, state: &mut AnalyzePhotosState) -> JobResult<()> { + for photo_id in &state.photo_ids { + let record = ctx.vdfs().get_record(*photo_id).await?; + let Some(content_uuid) = record.content_uuid() else { continue }; + match ctx.run(detect_faces_in_photo, record).await { + Ok(faces) => ctx.save_sidecar(content_uuid, "faces", &faces).await?, + Err(Error::NotAvailable(_)) => { ctx.add_warning("no face detector"); break } + Err(e) => return Err(e), + } } Ok(()) } ``` -### Test Extension (Still Works!) +### Test Extension ```rust #[job(name = "counter")] @@ -400,98 +418,32 @@ fn test_counter(ctx: &JobContext, state: &mut CounterState) -> Result<()> { | Module | Status | Notes | |--------|--------|-------| -| `ffi.rs` | Implemented | Low-level WASM imports | -| `job_context.rs` | Expanded | New methods added, existing preserved | -| `types.rs` | Expanded | All common types added | -| `vdfs.rs` | Stubs | Type-checks, `todo!()` for host calls | -| `ai.rs` | Stubs | Type-checks, `todo!()` for inference | -| `agent.rs` | Stubs | Type-checks, memory system defined | -| `models.rs` | Stubs | Type-checks, registration stubs | -| `actions.rs` | Stubs | Type-checks, preview/execute defined | -| `tasks.rs` | Stubs | Type-checks, task context defined | -| `query.rs` | Stubs | Type-checks, query context defined | - -**Macros:** All defined as pass-through (no codegen yet) +| `ffi.rs` | Implemented | `spacedrive_log`, `register_job`, `register_model`, `spacedrive_random`, `spacedrive_now_ms`, `spacedrive_op` | +| `job_context.rs` | Implemented | Progress, checkpoints, interrupts, sidecars, `run` for tasks | +| `types.rs` | Implemented | `Record`, `RecordKind`, `Error` with host codes | +| `vdfs.rs` | Partly | Records, sidecars and models work; tags and custom fields return `NotAvailable` | +| `clock.rs` | Implemented | `now()` over `spacedrive_now_ms` | +| `tasks.rs` | Implemented | `#[task]` with retry policy and deadline; the host logs each attempt | +| `models.rs` | Implemented | `#[model]` defines, `#[extension(models = [...])]` registers; rows in the extension's store | +| `ai.rs` | Not available | `infer` checks the `use_models` grant on the host, then returns `Error::NotAvailable` | +| `config.rs` | Implemented | `config.json` beside the manifest | +| `agent.rs` | Stubs | Memory and notifications have no host side | +| `actions.rs` | Stubs | Preview and execute have no host side | +| `query.rs` | Stubs | No host side | --- ## What Works -**Type-checking:** Photos extension compiles and type-checks -**Test extension:** Existing test-extension still works -**API surface:** Complete API from specification available -**Documentation:** IntelliSense/rust-analyzer autocomplete works +**Runtime:** The photos extension's `analyze_photos` runs end to end under the daemon's `wasm` feature; `core/tests/wasm_extension_test.rs` is the acceptance test. +**Randomness:** `getrandom` is backed by `spacedrive_random`, so `Uuid::new_v4()` and `rand` work in the guest (each extension's `.cargo/config.toml` sets `--cfg getrandom_backend="custom"`). ## What Doesn't Work Yet -**Runtime:** All new methods are `todo!()` - will panic if called -**Host functions:** WASM imports not implemented in Core -**Macro codegen:** Macros don't generate code yet -**Memory persistence:** No storage backend - ---- - -## Next Steps for Implementation - -### Phase 1: Core Host Functions - -Implement in `core/src/infra/extension/host_functions.rs`: -```rust -#[no_mangle] -pub extern "C" fn vdfs_query_entries(...) -> u32; -#[no_mangle] -pub extern "C" fn model_create(...) -> u32; -#[no_mangle] -pub extern "C" fn model_get_by_content(...) -> u32; -#[no_mangle] -pub extern "C" fn add_tag_to_content(...) -> u32; -#[no_mangle] -pub extern "C" fn model_register(...) -> u32; -#[no_mangle] -pub extern "C" fn ai_infer(...) -> u32; -``` - -### Phase 2: SDK Implementation - -Replace `todo!()` with actual WASM host calls: -```rust -pub async fn get_model_by_content(content_uuid: Uuid) -> Result { - // Serialize request - let request = ModelRequest { content_uuid, model_type: T::MODEL_TYPE }; - let req_bytes = serde_json::to_vec(&request)?; - - // Call host function - let result_ptr = unsafe { - model_get_by_content(req_bytes.as_ptr(), req_bytes.len()) - }; - - // Deserialize response - let response_bytes = unsafe { read_host_memory(result_ptr) }; - let model: T = serde_json::from_slice(&response_bytes)?; - - Ok(model) -} -``` - -### Phase 3: Macro Code Generation - -Implement real macros: -```rust -#[model(scope = "content")] -struct PhotoAnalysis { ... } - -// Generates: -impl ExtensionModel for PhotoAnalysis { - const MODEL_TYPE: &'static str = "PhotoAnalysis"; - fn uuid(&self) -> Uuid { self.id } -} - -impl PhotoAnalysis { - pub async fn save_for_content(ctx: &VdfsContext, content_uuid: Uuid, self) -> Result<()> { - ctx.create_model_for_content(content_uuid, self).await - } -} -``` +**Inference:** The core has no provider for face detection, scene classification, embeddings or language models, so every `ai().infer` answers `NotAvailable`; photos' scenes, places and moments jobs have nothing to run on. +**Tags, custom fields, dispatching jobs:** no host side. +**Agents, actions, queries:** no host side. +**File kinds and previews:** not registered by extensions. --- @@ -525,7 +477,7 @@ cargo build --target wasm32-unknown-unknown --release ## Breaking Changes -**None!** The existing test-extension API is preserved: +See the renames table in the overview. The original test-extension API is preserved: - `ctx.log()` - `ctx.checkpoint()` - `ctx.check_interrupt()` @@ -537,5 +489,5 @@ New methods are additive only. --- -**The SDK is now complete for type-checking. Extensions can be written and will compile. Runtime implementation is the next phase.** +**Extensions compile and the record, sidecar, model and task paths run. Inference is the next host side to build.** diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index f8e8da5ad..dc2eafbb9 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -9,6 +9,7 @@ version = "0.1.0" [dependencies] chrono = { version = "0.4", default-features = false, features = ["serde"] } +getrandom = { version = "0.4", default-features = false } serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } spacedrive-sdk-macros = { path = "../sdk-macros" } diff --git a/crates/sdk/src/actions.rs b/crates/sdk/src/actions.rs index ec443641f..d30b378fd 100644 --- a/crates/sdk/src/actions.rs +++ b/crates/sdk/src/actions.rs @@ -49,8 +49,8 @@ pub enum Change { name: String, parent: uuid::Uuid, }, - MoveEntry { - entry: Entry, + MoveRecord { + record: Record, destination: String, }, } diff --git a/crates/sdk/src/ai.rs b/crates/sdk/src/ai.rs index 18ab6db10..055aca2d4 100644 --- a/crates/sdk/src/ai.rs +++ b/crates/sdk/src/ai.rs @@ -3,7 +3,12 @@ //! This handles ML models (face_detection, llm, ocr, embeddings). //! NOT to be confused with data models (Person, Album) - those are in models.rs //! -//! Stubs for type-checking. Implementation will call WASM host functions. +//! Inference goes to the host as `ai.infer`, which checks the manifest's +//! `use_models` grant and hands the input to whatever provider the core has +//! for the model's category. The core has no face detection, scene +//! classification, embedding or language model provider today, so those +//! calls return [`Error::NotAvailable`], which a job handles rather than +//! fails on. use crate::types::*; use serde::{de::DeserializeOwned, Serialize}; @@ -38,7 +43,7 @@ impl AiModelRegistry { name: &str, source: AiModelSource, ) -> Result { - panic!("WASM host call") + Err(Error::Unsupported("register".into())) } /// Check if AI model is registered @@ -91,24 +96,32 @@ impl ModelHandle { } } + fn infer(&self, task: &str, input: &[u8]) -> Result { + crate::ffi::op_framed( + "ai.infer", + &serde_json::json!({ "model": self.model_id, "task": task }), + input, + ) + } + /// Detect faces in image pub async fn detect_faces(&self, image_data: &[u8]) -> Result> { - panic!("WASM host call") + self.infer("detect_faces", image_data) } /// Classify scene in image pub async fn classify(&self, image_data: &[u8]) -> Result> { - panic!("WASM host call") + self.infer("classify", image_data) } /// OCR document - pub async fn ocr_document(&self, entry: &Entry) -> Result { - panic!("WASM host call") + pub async fn ocr_document(&self, record: &Record) -> Result { + Err(Error::Unsupported("ocr_document".into())) } /// Generate text embedding pub async fn embed_text(&self, text: &str) -> Result> { - panic!("WASM host call") + self.infer("embed_text", text.as_bytes()) } } @@ -121,7 +134,7 @@ pub struct PromptBuilder { impl PromptBuilder { /// Render template with context pub fn render_with(self, context: &T) -> Result { - panic!("Render Jinja template") + Err(Error::Unsupported("render_with".into())) } } @@ -134,12 +147,12 @@ pub struct RenderedPrompt { impl RenderedPrompt { /// Generate text from rendered prompt pub async fn generate_text(self) -> Result { - panic!("WASM host call - LLM inference") + Err(Error::Unsupported("generate_text".into())) } /// Generate JSON from rendered prompt pub async fn generate_json(self) -> Result { - panic!("WASM host call - structured output") + Err(Error::Unsupported("generate_json".into())) } } diff --git a/crates/sdk/src/clock.rs b/crates/sdk/src/clock.rs new file mode 100644 index 000000000..afd22e691 --- /dev/null +++ b/crates/sdk/src/clock.rs @@ -0,0 +1,16 @@ +//! The host's clock +//! +//! wasm32-unknown-unknown has no clock, so `Utc::now()` and +//! `SystemTime::now()` trap in a guest. Extensions ask the host instead. + +use chrono::{DateTime, Utc}; + +/// The current time, from the host. +pub fn now() -> DateTime { + DateTime::from_timestamp_millis(crate::ffi::now_ms()).unwrap_or_default() +} + +/// Milliseconds since the Unix epoch, from the host. +pub fn now_ms() -> i64 { + crate::ffi::now_ms() +} diff --git a/crates/sdk/src/config.rs b/crates/sdk/src/config.rs new file mode 100644 index 000000000..18f16f124 --- /dev/null +++ b/crates/sdk/src/config.rs @@ -0,0 +1,17 @@ +//! Extension configuration +//! +//! The host reads `config.json` beside the extension's manifest and answers +//! `config.get` with it, or with `{}` when there is none. + +use serde::de::DeserializeOwned; + +/// Load the configuration once per call and leak it: a config is read a +/// handful of times per job and the guest's heap goes with the job's +/// instance, so a reference is the convenient shape and the leak is bounded. +pub fn load() -> &'static C { + let value: serde_json::Value = crate::ffi::op_json("config.get", &serde_json::json!({})) + .unwrap_or_else(|e| panic!("extension config could not be read from the host: {e}")); + let config: C = serde_json::from_value(value) + .unwrap_or_else(|e| panic!("extension config.json does not match the config type: {e}")); + Box::leak(Box::new(config)) +} diff --git a/crates/sdk/src/ffi.rs b/crates/sdk/src/ffi.rs index d1d186cc7..a69d81989 100644 --- a/crates/sdk/src/ffi.rs +++ b/crates/sdk/src/ffi.rs @@ -1,6 +1,16 @@ //! Low-level FFI bindings to Spacedrive host functions //! //! This module is internal - extension developers should use the high-level API. +//! +//! Every host function that returns data goes through [`op`]: the guest names +//! an operation and hands over a payload, the host writes the result into +//! memory it takes from [`wasm_alloc`] and reports where. One convention for +//! every operation keeps the import table small and the memory handling in +//! one place on each side. + +use serde::{de::DeserializeOwned, Serialize}; + +use crate::types::{Error, Result}; // Import Spacedrive host functions #[link(wasm_import_module = "spacedrive")] @@ -13,6 +23,16 @@ extern "C" { export_fn_len: u32, resumable: u32, ) -> i32; + fn register_model(def_ptr: *const u8, def_len: u32) -> i32; + fn spacedrive_random(buf_ptr: *mut u8, buf_len: u32); + fn spacedrive_now_ms() -> i64; + fn spacedrive_op( + op_ptr: *const u8, + op_len: u32, + payload_ptr: *const u8, + payload_len: u32, + out_ptr: *mut u32, + ) -> i32; } /// Log a message (info level) @@ -63,7 +83,7 @@ pub unsafe extern "C" fn wasm_free(ptr: *mut u8, size: i32) { /// Register a job with the extension system /// /// Called automatically by #[extension] macro during plugin_init() -pub fn register_job_with_host(job_name: &str, export_fn: &str, resumable: bool) -> Result<(), ()> { +pub fn register_job_with_host(job_name: &str, export_fn: &str, resumable: bool) -> Result<()> { let result = unsafe { register_job( job_name.as_ptr(), @@ -77,10 +97,120 @@ pub fn register_job_with_host(job_name: &str, export_fn: &str, resumable: bool) if result == 0 { Ok(()) } else { - Err(()) + Err(Error::HostCall(format!("register_job({job_name})"))) } } +/// Declare a data model's facet to the host, from `plugin_init`. +/// +/// `def_json` is the model definition the `#[model]` macro derived from the +/// struct; the host turns it into a facet table in the extension's store. +pub fn register_model_with_host(def_json: &str) -> Result<()> { + let result = unsafe { register_model(def_json.as_ptr(), def_json.len() as u32) }; + if result == 0 { + Ok(()) + } else { + Err(Error::HostCall("register_model".into())) + } +} + +/// Fill a buffer with entropy from the host. +pub fn random_bytes(buf: &mut [u8]) { + unsafe { spacedrive_random(buf.as_mut_ptr(), buf.len() as u32) } +} + +/// The host's clock, as milliseconds since the Unix epoch. +pub fn now_ms() -> i64 { + unsafe { spacedrive_now_ms() } +} + +/// The entropy source behind `getrandom`, and so behind `Uuid::new_v4` and +/// `rand`, in a guest built with `--cfg getrandom_backend="custom"`. +/// +/// wasm32-unknown-unknown has no entropy of its own; this routes every +/// request to the host. The symbol name is getrandom's contract. +#[no_mangle] +unsafe extern "Rust" fn __getrandom_v03_custom( + dest: *mut u8, + len: usize, +) -> ::core::result::Result<(), getrandom::Error> { + let buf = unsafe { std::slice::from_raw_parts_mut(dest, len) }; + random_bytes(buf); + Ok(()) +} + +/// The shape of an error the host reports from an operation. +#[derive(serde::Deserialize)] +struct HostError { + code: String, + message: String, +} + +/// Run one host operation and return the bytes it answered with. +/// +/// The host allocates the answer through `wasm_alloc` and reports pointer and +/// length through `out`; this copies the bytes into a `Vec` and frees the +/// host's buffer, so callers never touch raw guest memory. A non-zero status +/// means the bytes are a JSON error, mapped onto [`Error`] by its code. +pub fn op(op: &str, payload: &[u8]) -> Result> { + let mut out = [0u32; 2]; + let status = unsafe { + spacedrive_op( + op.as_ptr(), + op.len() as u32, + payload.as_ptr(), + payload.len() as u32, + out.as_mut_ptr(), + ) + }; + let (ptr, len) = (out[0] as *mut u8, out[1] as usize); + let bytes = if ptr.is_null() || len == 0 { + Vec::new() + } else { + let bytes = unsafe { std::slice::from_raw_parts(ptr, len) }.to_vec(); + unsafe { wasm_free(ptr, len.max(1) as i32) }; + bytes + }; + if status == 0 { + return Ok(bytes); + } + let error: HostError = serde_json::from_slice(&bytes) + .map_err(|_| Error::HostCall(format!("{op}: host error {status} was not readable")))?; + Err(match error.code.as_str() { + "not_found" => Error::NotFound, + "permission_denied" => Error::PermissionDenied(error.message), + "invalid_input" => Error::InvalidInput(error.message), + "not_available" => Error::NotAvailable(error.message), + "timeout" => Error::Timeout(error.message), + "interrupted" => Error::Interrupted, + _ => Error::OperationFailed(format!("{op}: {}", error.message)), + }) +} + +/// [`op`] with a JSON header followed by raw bytes, for inputs too large to +/// base64 into JSON: the frame is the header's length as little-endian u32, +/// the header, then the bytes. The answer is JSON. +pub fn op_framed( + op_name: &str, + header: &I, + bytes: &[u8], +) -> Result { + let header = serde_json::to_vec(header).map_err(|e| Error::Serialization(e.to_string()))?; + let mut payload = Vec::with_capacity(4 + header.len() + bytes.len()); + payload.extend_from_slice(&(header.len() as u32).to_le_bytes()); + payload.extend_from_slice(&header); + payload.extend_from_slice(bytes); + let answer = op(op_name, &payload)?; + serde_json::from_slice(&answer).map_err(|e| Error::Deserialization(format!("{op_name}: {e}"))) +} + +/// [`op`] with a JSON request and a JSON answer. +pub fn op_json(op_name: &str, input: &I) -> Result { + let payload = serde_json::to_vec(input).map_err(|e| Error::Serialization(e.to_string()))?; + let bytes = op(op_name, &payload)?; + serde_json::from_slice(&bytes).map_err(|e| Error::Deserialization(format!("{op_name}: {e}"))) +} + /// Drive a future to completion on the guest's single thread. /// /// Every host call returns before the guest continues, so a future built from diff --git a/crates/sdk/src/job_context.rs b/crates/sdk/src/job_context.rs index a91b73431..ed3e744a8 100644 --- a/crates/sdk/src/job_context.rs +++ b/crates/sdk/src/job_context.rs @@ -97,7 +97,7 @@ impl JobContext { /// Check for interruption (async version) pub async fn check_interrupt(&self) -> crate::types::Result<()> { if self.check_interrupt_sync() { - Err(crate::types::Error::OperationFailed("Interrupted".into())) + Err(crate::types::Error::Interrupted) } else { Ok(()) } @@ -144,13 +144,21 @@ impl JobContext { self.ai_models() } - /// Run a task (for job composition) - pub async fn run(&self, _task: F, _args: A) -> crate::types::Result + /// Run a task, retrying and timing out as its `#[task]` attribute says. + /// + /// The host enforces the timeout: once an attempt's deadline passes, every + /// host operation the task makes fails with `Error::Timeout`, and the next + /// attempt starts if the policy allows one. Each attempt is written to the + /// job log. + pub async fn run( + &self, + _task: T, + args: T::Args, + ) -> crate::types::Result where - F: Fn(crate::tasks::TaskContext, A) -> Fut, - Fut: std::future::Future>, + T::Args: Clone, { - panic!("Execute task with checkpoint") + crate::tasks::run::(args).await } /// Report progress @@ -168,24 +176,40 @@ impl JobContext { } } - /// Check if sidecar exists + /// Whether a sidecar of one of this extension's kinds exists for the + /// content. pub fn sidecar_exists( &self, content_uuid: uuid::Uuid, kind: &str, ) -> crate::types::Result { - panic!("WASM host call") + crate::vdfs::VdfsContext.sidecar_exists(content_uuid, kind) } - /// Save sidecar + /// Write a sidecar of one of this extension's kinds, as JSON, keyed by + /// the content it describes. The kind must be in the manifest's + /// `write_sidecars`; the host namespaces it by extension, which is why + /// no extension id is passed here any more. pub async fn save_sidecar( &self, content_uuid: uuid::Uuid, kind: &str, - extension_id: &str, data: &T, ) -> crate::types::Result<()> { - panic!("WASM host call") + crate::vdfs::VdfsContext + .write_sidecar(content_uuid, kind, data) + .await + } + + /// Read back a sidecar this extension may read. + pub async fn read_sidecar( + &self, + content_uuid: uuid::Uuid, + kind: &str, + ) -> crate::types::Result { + crate::vdfs::VdfsContext + .read_sidecar(content_uuid, kind) + .await } /// Access agent memory @@ -193,9 +217,12 @@ impl JobContext { panic!("Access agent memory") } - /// Access extension config - pub fn config(&self) -> &C { - panic!("Access config") + /// The extension's configuration: `config.json` beside its manifest, + /// or `{}` when there is none, so a config type with serde defaults + /// always loads. A config that does not deserialize is a panic, since + /// the job cannot do anything sensible without it. + pub fn config(&self) -> &C { + crate::config::load() } /// Send notification diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index cfdb29793..6a79c2919 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -4,29 +4,16 @@ //! //! # Example //! -//! This sketches the intended API. `ExtensionContext`, `CreateEntry` and -//! `OcrOptions` do not exist yet, so the example is not compiled. -//! //! ```ignore -//! use spacedrive_sdk::{ExtensionContext, prelude::*}; -//! -//! #[spacedrive_extension] -//! fn init(ctx: &mut ExtensionContext) -> Result<()> { -//! ctx.log("Finance extension starting..."); -//! -//! // Create entry -//! let entry = ctx.vdfs().create_entry(CreateEntry { -//! name: "Receipt: Starbucks".into(), -//! path: "receipts/1.eml".into(), -//! entry_type: "FinancialDocument".into(), -//! })?; -//! -//! // Run OCR -//! let ocr_result = ctx.ai().ocr(&pdf_data, OcrOptions::default())?; -//! -//! // Store sidecar -//! ctx.vdfs().write_sidecar(entry.id, "ocr.txt", ocr_result.text.as_bytes())?; +//! use spacedrive_sdk::prelude::*; //! +//! #[job(name = "sizes")] +//! async fn sizes(ctx: &JobContext, state: &mut SizesState) -> Result<()> { +//! let photos = ctx.vdfs().query_records().of_type::().collect().await?; +//! for photo in photos { +//! let bytes = photo.read().await?; +//! ctx.log(&format!("{} is {} bytes", photo.name(), bytes.len())); +//! } //! Ok(()) //! } //! ``` @@ -36,6 +23,8 @@ pub mod actions; pub mod agent; pub mod ai; +pub mod clock; +pub mod config; pub mod ffi; pub mod job_context; pub mod models; @@ -52,6 +41,7 @@ pub use agent::{ NotificationBuilder, TemporalMemory, TemporalQuery, WorkingMemory, }; pub use ai::*; +pub use clock::now; pub use job_context::JobContext as SdkJobContext; pub use models::*; pub use query::*; @@ -64,6 +54,7 @@ pub mod prelude { pub use crate::actions::*; pub use crate::agent::*; pub use crate::ai::*; + pub use crate::clock::now; pub use crate::job_context::{JobContext, JobResult}; pub use crate::models::*; pub use crate::query::*; diff --git a/crates/sdk/src/models.rs b/crates/sdk/src/models.rs index f5cd7879d..57e47edad 100644 --- a/crates/sdk/src/models.rs +++ b/crates/sdk/src/models.rs @@ -8,16 +8,22 @@ use crate::types::*; use serde::{de::DeserializeOwned, Serialize}; -/// Marker trait for extension-defined data models +/// An extension-defined data model, implemented by `#[model]`. /// -/// Models are stored in the `models` database table and can be: -/// - Content-scoped: Attached to a ContentIdentity (PhotoAnalysis) -/// - Standalone: Independent entities (Person, Album, Place) -/// - Entry-scoped: Tied to a specific path (rare) +/// A model is a record in the extension's own store with a facet table of +/// the struct's fields. It is either standalone, addressed by its own uuid +/// (Person, Album, Place), or content-scoped, addressed by the content it +/// describes (an analysis of a photo). `plugin_init` declares every model +/// listed in `#[extension(models = [...])]` to the host, which creates or +/// widens the facet table before any job runs. pub trait ExtensionModel: Serialize + DeserializeOwned + Send + Sync { /// Model type name (e.g., "Person", "Album") const MODEL_TYPE: &'static str; + /// The facet definition the host registers: the fields and their column + /// types, as JSON. Derived from the struct by `#[model]`. + const DEFINITION: &'static str; + /// Get the model's UUID fn uuid(&self) -> Uuid; diff --git a/crates/sdk/src/tasks.rs b/crates/sdk/src/tasks.rs index 9895e8ad1..a0c100928 100644 --- a/crates/sdk/src/tasks.rs +++ b/crates/sdk/src/tasks.rs @@ -1,14 +1,103 @@ //! Task system for job composition //! -//! Stubs for type-checking. Implementation will call WASM host functions. +//! A task is a unit of work a job runs through [`crate::job_context::JobContext::run`]. +//! The `#[task]` attribute turns an async function into a unit struct that +//! implements [`Task`], carrying the retry and timeout policy the attribute +//! declares. The policy is enforced by the host: `task.begin` hands the host +//! the deadline, and every host operation the task makes after the deadline +//! fails with [`Error::Timeout`], which is the only way a single-threaded +//! guest can be stopped mid-task. Retries are the guest's loop over attempts. + +use serde::{Deserialize, Serialize}; use crate::types::*; use crate::{ai::AiContext, vdfs::VdfsContext}; +/// A unit of work with a retry and timeout policy. +/// +/// Implemented by the `#[task]` attribute; extensions do not implement it by +/// hand. +pub trait Task { + type Args; + type Output; + + /// The function's name, as the job log reports it. + const NAME: &'static str; + /// Attempts after the first one, on a retryable error. + const RETRIES: u32; + /// Deadline per attempt; 0 means none. + const TIMEOUT_MS: u64; + + async fn call(ctx: TaskContext, args: Self::Args) -> TaskResult; +} + +/// What the guest tells the host when a task attempt starts. +#[derive(Serialize)] +pub(crate) struct TaskBegin<'a> { + pub name: &'a str, + pub attempt: u32, + pub retries: u32, + pub timeout_ms: u64, +} + +/// What the guest tells the host when an attempt ends. +#[derive(Serialize)] +pub(crate) struct TaskEnd<'a> { + pub name: &'a str, + pub attempt: u32, + pub error: Option<&'a str>, +} + +#[derive(Deserialize)] +pub(crate) struct TaskHandle { + pub id: u32, +} + +/// Run a task to completion, retrying on retryable errors as its policy says. +pub(crate) async fn run(args: T::Args) -> Result +where + T::Args: Clone, +{ + let mut attempt = 0; + loop { + let handle: TaskHandle = crate::ffi::op_json( + "task.begin", + &TaskBegin { + name: T::NAME, + attempt, + retries: T::RETRIES, + timeout_ms: T::TIMEOUT_MS, + }, + )?; + let result = T::call(TaskContext { task_id: handle.id }, args.clone()).await; + let error = result.as_ref().err().map(|e| e.to_string()); + let _: serde_json::Value = crate::ffi::op_json( + "task.end", + &TaskEnd { + name: T::NAME, + attempt, + error: error.as_deref(), + }, + )?; + match result { + Ok(output) => return Ok(output), + Err(error) if attempt < T::RETRIES && error.is_retryable() => attempt += 1, + Err(error) => return Err(error), + } + } +} + /// Task execution context -pub struct TaskContext; +pub struct TaskContext { + task_id: u32, +} impl TaskContext { + /// The host's handle for this attempt. + pub fn task_id(&self) -> u32 { + self.task_id + } + /// Access VDFS pub fn vdfs(&self) -> VdfsContext { VdfsContext @@ -19,9 +108,12 @@ impl TaskContext { AiContext } - /// Access extension config - pub fn config(&self) -> &C { - panic!("Access config") + /// The extension's configuration: `config.json` beside its manifest, + /// or `{}` when there is none, so a config type with serde defaults + /// always loads. A config that does not deserialize is a panic, since + /// the job cannot do anything sensible without it. + pub fn config(&self) -> &C { + crate::config::load() } /// Read sidecar data @@ -30,7 +122,7 @@ impl TaskContext { content_uuid: Uuid, kind: &str, ) -> Result { - panic!("WASM host call") + VdfsContext.read_sidecar(content_uuid, kind).await } } diff --git a/crates/sdk/src/types.rs b/crates/sdk/src/types.rs index 4dc1d0368..2d333cdf3 100644 --- a/crates/sdk/src/types.rs +++ b/crates/sdk/src/types.rs @@ -32,6 +32,36 @@ pub enum Error { #[error("Not found")] NotFound, + + /// The host has no provider for what was asked: no model of that kind is + /// installed, no tool is present. The call was understood and refused. + #[error("Not available: {0}")] + NotAvailable(String), + + /// The SDK declares this call but no host function backs it yet. + #[error("Unsupported by this host: {0}")] + Unsupported(String), + + /// A task ran past the timeout its `#[task]` attribute declares. + #[error("Timed out: {0}")] + Timeout(String), + + /// The job was asked to pause or cancel. + #[error("Interrupted")] + Interrupted, +} + +impl Error { + /// Whether a task that failed with this error is worth running again. + /// + /// A refusal, a missing provider or an interrupt will come back the same; + /// a timeout or a failed operation might not. + pub fn is_retryable(&self) -> bool { + matches!( + self, + Error::Timeout(_) | Error::OperationFailed(_) | Error::HostCall(_) + ) + } } /// Result type for SDK operations @@ -46,63 +76,72 @@ pub type JobResult = std::result::Result; /// Query result type pub type QueryResult = std::result::Result; -/// Entry in VDFS (file, directory, or virtual) +/// A record in a source store: a file, directory or symlink an ingest saw. +/// +/// This is the SDK's view of `sd_store::Record` with its filesystem facet. +/// Records replace the old `Entry`: a record's identity survives a move, its +/// bytes are identified by `content_uuid` once hashing has reached them, and +/// its path is relative to the source that holds it. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Entry { - pub id: Uuid, - pub uuid: Option, +pub struct Record { + pub uuid: Uuid, + /// The source whose store holds this record. + pub source_id: Uuid, pub name: String, - pub kind: EntryKind, + pub kind: RecordKind, + /// Lowercase, without the dot. pub extension: Option, - pub metadata_id: Option, - pub content_id: Option, - pub size: i64, + /// Relative to the source root, `/`-separated. + pub relative_path: String, + pub size: Option, + /// Unix milliseconds. + pub modified_ms: Option, + /// The identity of the bytes, once the hash job has reached them. + pub content_uuid: Option, } -impl Entry { - /// Get entry UUID +impl Record { + /// The record uuid. pub fn id(&self) -> Uuid { - self.uuid.unwrap_or(self.id) + self.uuid } - /// Get content UUID (for content-scoped operations) + /// The identity of the bytes, for content-scoped sidecars and models. + /// `None` means the file has not been hashed yet. pub fn content_uuid(&self) -> Option { - panic!("WASM host call not implemented") + self.content_uuid } - /// Get metadata ID - pub fn metadata_id(&self) -> i32 { - self.metadata_id.unwrap_or(0) - } - - /// Get entry name pub fn name(&self) -> &str { &self.name } - /// Get entry path - pub fn path(&self) -> String { - panic!("WASM host call not implemented") + /// The source-relative path. + pub fn path(&self) -> &str { + &self.relative_path } - /// Read entry data + /// The record's bytes, read through the source's resolved path. + /// + /// Needs the `read_records` grant for this record's extension; fails + /// with `NotFound` when the source is detached. pub async fn read(&self) -> Result> { - panic!("WASM host call not implemented") + crate::vdfs::VdfsContext.read_record(self.uuid).await } - /// Get custom field from entry's metadata + /// Get custom field from the record's metadata pub fn custom_field(&self, field: &str) -> Result { - panic!("WASM host call not implemented") + Err(Error::Unsupported("custom_field".into())) } } -/// Entry kind +/// What the ingest found at the record's path. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum EntryKind { - File = 0, - Directory = 1, - Symlink = 2, - Virtual = 3, +#[serde(rename_all = "lowercase")] +pub enum RecordKind { + File, + Directory, + Symlink, } /// Tag diff --git a/crates/sdk/src/vdfs.rs b/crates/sdk/src/vdfs.rs index a96d357da..c910e78d7 100644 --- a/crates/sdk/src/vdfs.rs +++ b/crates/sdk/src/vdfs.rs @@ -1,6 +1,9 @@ -//! VDFS operations - queries, models, tags, collections +//! VDFS operations - records, models, tags, collections //! -//! Stubs for type-checking. Implementation will call WASM host functions. +//! Records are the files the library's source stores hold; see +//! [`crate::types::Record`]. Reading them needs the manifest's `read_records` +//! grant. Model and tag operations are stubs until a host function backs +//! them. use crate::types::*; use serde::{de::DeserializeOwned, Serialize}; @@ -9,35 +12,91 @@ use serde::{de::DeserializeOwned, Serialize}; pub struct VdfsContext; impl VdfsContext { - /// Query entries (files/directories) - pub fn query_entries(&self) -> EntryQuery { - EntryQuery::default() + /// Query records (files and directories) across the library's sources. + pub fn query_records(&self) -> RecordQuery { + RecordQuery::default() } - /// Get a specific entry by UUID - pub async fn get_entry(&self, uuid: Uuid) -> Result { - panic!("WASM host call not implemented") + /// One record by uuid, from whichever source store holds it. + pub async fn get_record(&self, uuid: Uuid) -> Result { + crate::ffi::op_json("records.get", &serde_json::json!({ "uuid": uuid })) + } + + /// A record's bytes, through the source's resolved path. + pub async fn read_record(&self, uuid: Uuid) -> Result> { + let payload = serde_json::to_vec(&serde_json::json!({ "uuid": uuid }))?; + crate::ffi::op("records.read", &payload) + } + + /// Whether a sidecar of one of this extension's kinds exists for the + /// content. + pub fn sidecar_exists(&self, content_uuid: Uuid, kind: &str) -> Result { + crate::ffi::op_json( + "sidecars.exists", + &serde_json::json!({ "content_uuid": content_uuid, "kind": kind }), + ) + } + + /// A sidecar this extension may read (`read_sidecars` or its own + /// `write_sidecars` kinds), decoded from JSON. + pub async fn read_sidecar( + &self, + content_uuid: Uuid, + kind: &str, + ) -> Result { + crate::ffi::op_json( + "sidecars.read", + &serde_json::json!({ "content_uuid": content_uuid, "kind": kind }), + ) + } + + /// Write a sidecar of one of this extension's `write_sidecars` kinds, as + /// JSON, replacing any earlier one for the same content. + pub async fn write_sidecar( + &self, + content_uuid: Uuid, + kind: &str, + data: &T, + ) -> Result<()> { + let _: serde_json::Value = crate::ffi::op_json( + "sidecars.write", + &serde_json::json!({ "content_uuid": content_uuid, "kind": kind, "data": data }), + )?; + Ok(()) } /// Query extension models pub fn query_models(&self) -> ModelQuery { ModelQuery { + filtered: false, + limit: None, _phantom: std::marker::PhantomData, } } /// Get model scoped to content_identity pub async fn get_model_by_content(&self, content_uuid: Uuid) -> Result { - panic!("WASM host call") + crate::ffi::op_json( + "models.get", + &serde_json::json!({ "model": T::MODEL_TYPE, "content_uuid": content_uuid }), + ) } - /// Create model scoped to content_identity + /// Create (or replace) the model describing one content identity. pub async fn create_model_for_content( &self, content_uuid: Uuid, model: T, ) -> Result<()> { - panic!("WASM host call") + let _: serde_json::Value = crate::ffi::op_json( + "models.put", + &serde_json::json!({ + "model": T::MODEL_TYPE, + "content_uuid": content_uuid, + "data": model, + }), + )?; + Ok(()) } /// Update model scoped to content @@ -49,17 +108,30 @@ impl VdfsContext { where F: FnOnce(T) -> Result, { - panic!("WASM host call") + let current = self.get_model_by_content::(content_uuid).await?; + self.create_model_for_content(content_uuid, f(current)?) + .await } - /// Create standalone model + /// Create (or replace) a standalone model, keyed by its own uuid. pub async fn create_model(&self, model: T) -> Result<()> { - panic!("WASM host call") + let _: serde_json::Value = crate::ffi::op_json( + "models.put", + &serde_json::json!({ + "model": T::MODEL_TYPE, + "uuid": model.uuid(), + "data": model, + }), + )?; + Ok(()) } /// Get standalone model by UUID pub async fn get_model(&self, uuid: Uuid) -> Result { - panic!("WASM host call") + crate::ffi::op_json( + "models.get", + &serde_json::json!({ "model": T::MODEL_TYPE, "uuid": uuid }), + ) } /// Update standalone model @@ -67,126 +139,175 @@ impl VdfsContext { where F: FnOnce(T) -> Result, { - panic!("WASM host call") + let current = self.get_model::(uuid).await?; + self.create_model(f(current)?).await } /// Add tag to content (all entries with this content get the tag) pub async fn add_tag_to_content(&self, content_uuid: Uuid, tag: &str) -> Result<()> { - panic!("WASM host call") + Err(Error::Unsupported("add_tag_to_content".into())) } /// Add tag to model pub async fn add_tag_to_model(&self, model_uuid: Uuid, tag: &str) -> Result<()> { - panic!("WASM host call") + Err(Error::Unsupported("add_tag_to_model".into())) } - /// Add tag to specific entry - pub async fn add_tag(&self, metadata_id: i32, tag: &str) -> Result<()> { - panic!("WASM host call") + /// Add tag to one record + pub async fn add_tag(&self, record_uuid: Uuid, tag: &str) -> Result<()> { + Err(Error::Unsupported("add_tag".into())) } /// Update custom field in UserMetadata pub async fn update_custom_field( &self, - entry_uuid: Uuid, + record_uuid: Uuid, field: &str, value: T, ) -> Result<()> { - panic!("WASM host call") + Err(Error::Unsupported("update_custom_field".into())) } - /// Check if entry is in user-granted scope + /// Check if a path is in user-granted scope pub fn in_granted_scope(&self, path: &str) -> bool { panic!("WASM host call") } } -/// Entry query builder -#[derive(Default)] -pub struct EntryQuery { - // Query state +/// Record query builder. +/// +/// Filters narrow the records a `collect` returns; the host answers from the +/// source stores of the job's library, newest sources first, in the order +/// each store walks its tree. +#[derive(Default, Serialize)] +pub struct RecordQuery { + source: Option, + scope: Option, + extensions: Option>, + tag: Option, + limit: Option, } -impl EntryQuery { - pub fn in_location(self, path: impl Into) -> Self { - panic!("Build query") +impl RecordQuery { + /// Only records of one source. + pub fn in_source(mut self, source: Uuid) -> Self { + self.source = Some(source); + self } - pub fn of_type(self) -> Self { - panic!("Filter by type") + /// Only records beneath a source-relative directory; "" is the root. + pub fn in_location(mut self, path: impl Into) -> Self { + self.scope = Some(path.into()); + self } - pub fn where_content_id(self, content_uuid: Uuid) -> Self { - panic!("Filter by content") + /// Only files with one of these extensions (case-insensitive). + pub fn with_extensions(mut self, extensions: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.extensions = Some( + extensions + .into_iter() + .map(|e| e.into().to_lowercase()) + .collect(), + ); + self } - pub fn on_this_device(self) -> Self { - panic!("Filter to local entries") + /// Only files whose extension `T` names; see [`Image`] and [`Pdf`]. + pub fn of_type(self) -> Self { + self.with_extensions(T::EXTENSIONS.iter().copied()) } - pub fn with_tag(self, tag: &str) -> Self { - panic!("Filter by tag") + /// Only records carrying this tag. No host side yet: a query with a tag + /// filter is refused rather than answered without it. + pub fn with_tag(mut self, tag: &str) -> Self { + self.tag = Some(tag.to_string()); + self } - pub fn with_sidecar(self, kind: &str) -> Self { - panic!("Filter by sidecar existence") + /// At most this many records; the host caps it at 10,000. + pub fn limit(mut self, limit: usize) -> Self { + self.limit = Some(limit); + self } pub fn where_metadata(self, field: &str, predicate: FieldPredicate) -> Self { panic!("Filter by metadata field") } - pub async fn first(self) -> Result> { - panic!("Execute query") + pub async fn first(self) -> Result> { + Ok(self.limit(1).collect().await?.into_iter().next()) } - pub async fn collect(self) -> Result> { - panic!("Execute query") - } - - pub fn map(self, f: F) -> MappedQuery - where - F: Fn(Entry) -> T, - { - panic!("Map results") + pub async fn collect(self) -> Result> { + crate::ffi::op_json("records.query", &self) } } -/// Mapped query results -pub struct MappedQuery { - _phantom: std::marker::PhantomData, +/// A file type a query can ask for by name. +pub trait FileType { + const EXTENSIONS: &'static [&'static str]; } -impl MappedQuery { - pub async fn collect(self) -> Result> { - panic!("Execute and map") - } +impl FileType for Image { + const EXTENSIONS: &'static [&'static str] = &[ + "jpg", "jpeg", "png", "gif", "webp", "heic", "heif", "tiff", "tif", "bmp", "avif", "raw", + "cr2", "nef", "dng", "arw", + ]; } -/// Model query builder +impl FileType for Pdf { + const EXTENSIONS: &'static [&'static str] = &["pdf"]; +} + +/// Model query builder. +/// +/// Only an unfiltered listing has a host side; a query with a field or +/// semantic filter is refused rather than answered without it. pub struct ModelQuery { + filtered: bool, + limit: Option, _phantom: std::marker::PhantomData, } impl ModelQuery { - pub fn where_field(self, field: &str, predicate: FieldPredicate) -> Self { - panic!("Filter by field") + pub fn where_field(mut self, field: &str, predicate: FieldPredicate) -> Self { + self.filtered = true; + self } - pub fn where_json_field(self, path: &str, predicate: FieldPredicate) -> Self { - panic!("Filter by JSON field") + pub fn where_json_field(mut self, path: &str, predicate: FieldPredicate) -> Self { + self.filtered = true; + self } - pub fn search_semantic(self, field: &str, query: SemanticQuery) -> Self { - panic!("Semantic search") + pub fn search_semantic(mut self, field: &str, query: SemanticQuery) -> Self { + self.filtered = true; + self + } + + pub fn limit(mut self, limit: usize) -> Self { + self.limit = Some(limit); + self } pub async fn first(self) -> Result> { - panic!("Execute query") + Ok(self.limit(1).collect().await?.into_iter().next()) } pub async fn collect(self) -> Result> { - panic!("Execute query") + if self.filtered { + return Err(Error::Unsupported( + "model queries cannot filter yet; list and filter in the guest".into(), + )); + } + crate::ffi::op_json( + "models.list", + &serde_json::json!({ "model": T::MODEL_TYPE, "limit": self.limit }), + ) } } diff --git a/crates/sidecar-path/src/lib.rs b/crates/sidecar-path/src/lib.rs index 833b5bcb3..449fc21b3 100644 --- a/crates/sidecar-path/src/lib.rs +++ b/crates/sidecar-path/src/lib.rs @@ -29,6 +29,12 @@ pub fn kind_directory(kind: &str) -> Option<&'static str> { } } +/// Directory for a kind an extension declared: under `extensions//` so +/// an extension's kinds never collide with the core's or another's. +pub fn extension_kind_directory(extension_id: &str, kind: &str) -> String { + format!("extensions/{extension_id}/{kind}") +} + /// Shard directories for a content UUID: the first two byte-pairs of the /// lowercase hex representation with hyphens removed. pub fn compute_shards(content_uuid: &Uuid) -> (String, String) { @@ -72,6 +78,22 @@ mod tests { ); } + #[test] + fn extension_kinds_sit_under_their_extension() { + let uuid = Uuid::parse_str("abcd1234-5678-90ab-cdef-123456789012").unwrap(); + assert_eq!( + relative_path( + &uuid, + &extension_kind_directory("com.spacedrive.photos", "faces"), + "default", + "json" + ), + PathBuf::from( + "content/ab/cd/abcd1234-5678-90ab-cdef-123456789012/extensions/com.spacedrive.photos/faces/default.json" + ) + ); + } + #[test] fn irregular_plurals_map_correctly() { assert_eq!(kind_directory("proxy"), Some("proxies")); diff --git a/crates/store/src/db.rs b/crates/store/src/db.rs index 447e774b5..da28aba31 100644 --- a/crates/store/src/db.rs +++ b/crates/store/src/db.rs @@ -14,7 +14,7 @@ use crate::content::ContentId; use crate::error::{Error, Result}; use crate::record::{facet_table, ContentIdentity, Record}; use crate::schema::codegen::indexed_search_fields; -use crate::schema::DataTypeSchema; +use crate::schema::{DataTypeSchema, FieldType}; /// `_sync_state` key holding the on-disk root a file-backed source's /// locator paths are relative to. File-backed adapters set it every sync; @@ -904,6 +904,79 @@ impl SourceDb { sql } + /// The declared fields of one model's records as JSON objects, typed back + /// the way they went in: a `json` field is parsed, a `boolean` is a bool, + /// numbers are numbers. `external_id` narrows to one record. Newest first. + /// + /// This is how an extension reads its own models back; the facet row is + /// the model, so nothing outside the declared fields comes out. + pub async fn facet_rows( + &self, + model: &str, + external_id: Option<&str>, + limit: usize, + ) -> Result> { + let model_def = self + .schema + .models + .get(model) + .ok_or_else(|| Error::Other(format!("unknown model: {model}")))?; + let table = facet_table(model); + let mut pairs = String::new(); + for name in model_def.fields.keys() { + if !pairs.is_empty() { + pairs.push_str(", "); + } + let _ = write!(pairs, "'{name}', f.\"{name}\""); + } + let pairs = if pairs.is_empty() { + String::new() + } else { + format!("json_object({pairs})") + }; + let sql = format!( + "SELECT r.uuid, {pairs} FROM record r \ + LEFT JOIN \"{table}\" f ON f.record_uuid = r.uuid \ + WHERE r.type = ?1 AND (?2 IS NULL OR r.external_id = ?2) \ + ORDER BY COALESCE(r.modified_at, r.created_at) DESC, r.rowid DESC \ + LIMIT ?3" + ); + let rows = sqlx::query_as::<_, (Uuid, Option)>(&sql) + .bind(model) + .bind(external_id) + .bind(limit as i64) + .fetch_all(&self.pool) + .await?; + + Ok(rows + .into_iter() + .filter_map(|(_, json)| { + let mut value: serde_json::Value = serde_json::from_str(&json?).ok()?; + let object = value.as_object_mut()?; + for (name, field_type) in &model_def.fields { + let Some(stored) = object.get_mut(name) else { + continue; + }; + match field_type { + FieldType::Json => { + if let Some(text) = stored.as_str() { + *stored = + serde_json::from_str(text).unwrap_or(serde_json::Value::Null); + } + } + FieldType::Boolean => { + if let Some(n) = stored.as_i64() { + *stored = serde_json::Value::Bool(n != 0); + } + } + _ => {} + } + } + Some(value) + }) + .collect()) + } + /// List items of the primary record type, newest first. /// Full record rows for the primary model — every facet field included — /// as JSON objects, newest first. Powers presentation surfaces that need diff --git a/extensions/README.md b/extensions/README.md index 7b22aa34b..fbd207828 100644 --- a/extensions/README.md +++ b/extensions/README.md @@ -81,12 +81,23 @@ cp target/wasm32-unknown-unknown/release/my_extension.wasm . "version": "0.1.0", "wasm_file": "my_extension.wasm", "permissions": { - "methods": ["vdfs.*", "ai.*"], - "libraries": ["*"] + "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): @@ -157,8 +168,8 @@ fn email_scan(ctx: &JobContext, state: &mut EmailScanState) -> Result<()> { ctx.add_warning("Non-fatal issue"); // Full SDK access - let entry = ctx.vdfs().create_entry(...)?; - let ocr = ctx.ai().ocr(&pdf, ...)?; + let record = ctx.vdfs().get_record(uuid).await?; + let bytes = record.read().await?; Ok(()) } @@ -167,32 +178,42 @@ fn email_scan(ctx: &JobContext, state: &mut EmailScanState) -> Result<()> { ### VDFS Operations ```rust -// Create entries -let entry = ctx.vdfs().create_entry(CreateEntry { - name: "My File".into(), - path: "path/to/file".into(), - entry_type: "Document".into(), - metadata: None, -})?; +// Records of the library's sources, filtered by extension (read_records grant) +let photos = ctx.vdfs() + .query_records() + .with_extensions(["jpg", "png"]) + .collect() + .await?; -// Write sidecars -ctx.vdfs().write_sidecar(entry.id, "metadata.json", data)?; +// One record and its bytes +let record = ctx.vdfs().get_record(uuid).await?; +let bytes = record.read().await?; -// Read sidecars -let data = ctx.vdfs().read_sidecar(entry.id, "metadata.json")?; +// 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 = 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. + ```rust -// OCR -let ocr = ctx.ai().ocr(&pdf_bytes, OcrOptions::default())?; - -// Classification -let result = ctx.ai().classify_text(&text, "Extract data")?; - -// Embeddings -let embedding = ctx.ai().embed("query text")?; +// Written against the API; answers NotAvailable until a provider exists +let faces = ctx.ai().from_registered("face_detection").detect_faces(&bytes).await?; ``` ### Credentials diff --git a/extensions/photos/.cargo/config.toml b/extensions/photos/.cargo/config.toml index 01c5e7c9c..efbd85bab 100644 --- a/extensions/photos/.cargo/config.toml +++ b/extensions/photos/.cargo/config.toml @@ -1,7 +1,6 @@ [build] target = "wasm32-unknown-unknown" -# The guest has no entropy source yet: the SDK exposes no host function for -# it, so a random id fails at runtime instead of failing the build. +# The guest has no entropy of its own; the SDK routes getrandom to the host. [target.wasm32-unknown-unknown] -rustflags = ["--cfg", "getrandom_backend=\"unsupported\""] +rustflags = ["--cfg", "getrandom_backend=\"custom\""] diff --git a/extensions/photos/README.md b/extensions/photos/README.md index 09f1a09f8..f89e95422 100644 --- a/extensions/photos/README.md +++ b/extensions/photos/README.md @@ -116,16 +116,14 @@ User can search "#person:alice" or "photos from beach" └── resnet50.onnx (95MB) .sdlibrary/ - └── sidecars/ - ├── content/{uuid}/ - │ └── extensions/photos/ - │ ├── faces.json # Face detection results - │ ├── scene.json # Scene classification - │ └── aesthetics.json # Quality score - └── extension/photos/ - └── memory/ - ├── history.db # Photo analysis events - └── knowledge.vss # Face/place graph + ├── sidecars/ + │ └── content/{ab}/{cd}/{uuid}/ + │ └── extensions/com.spacedrive.photos/ + │ ├── faces/default.json # Face detection results + │ ├── scene/default.json # Scene classification + │ └── aesthetics/default.json # Quality score + └── extensions/com.spacedrive.photos/ + └── data.db # Person, Place, Moment, Album, Photo rows ``` ## SDK Features Demonstrated @@ -147,9 +145,24 @@ User can search "#person:alice" or "photos from beach" - Tag generation from sidecars - Custom memory query methods -### SDK Features Used (Not Yet Implemented in Core) +### What runs today -Most features here are aspirational - the SDK is still being built. This serves as a comprehensive reference implementation. +Under a daemon built with `--features wasm`, `analyze_photos` runs end to end +over a library: it reads each photo through its record, asks for face +detection, and because the core has no inference provider the host answers +`not_available`; the job warns once, skips, and completes, so a later run with +a detector picks the photos up. `core/tests/wasm_extension_test.rs` proves +this against a fixture library. + +### Not yet backed by the core + +- Inference. There is no provider for face detection, scene classification, + embeddings or language models, so `analyze_scenes`, `identify_places` and + `create_moments` have nothing to run on. +- Tags, custom fields and `dispatch_jobs`. +- Agents, actions and queries. +- Model downloads named in `manifest.json`. +- File-kind and preview registration. ## Capabilities Compared diff --git a/extensions/photos/manifest.json b/extensions/photos/manifest.json index f6985ea0b..347cbb380 100644 --- a/extensions/photos/manifest.json +++ b/extensions/photos/manifest.json @@ -12,7 +12,7 @@ "semantic_search" ], "permissions": { - "read_entries": { + "read_records": { "glob": "**/*.{jpg,jpeg,png,heic,heif,raw,cr2,nef,dng,webp}" }, "read_sidecars": [ diff --git a/extensions/photos/photos.wasm b/extensions/photos/photos.wasm index 7710e2848682d272aa7f151185f98b01b357d97b..cb48e53b1d29a63e5f3612144a5a09f07aa5cfb2 100755 GIT binary patch literal 332460 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%0A+wQgfB0q!U6>C>DgSwy`MG-izC+ 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 Q{BLAX9||-a0!!5Y0l8xK+5i9m delta 51675 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 A6aWAK diff --git a/extensions/photos/src/actions/create_album.rs b/extensions/photos/src/actions/create_album.rs index eab352787..77692c59b 100644 --- a/extensions/photos/src/actions/create_album.rs +++ b/extensions/photos/src/actions/create_album.rs @@ -1,6 +1,5 @@ use spacedrive_sdk::{action, action_execute}; -use chrono::Utc; use spacedrive_sdk::prelude::*; use uuid::Uuid; @@ -22,7 +21,7 @@ pub async fn create_album( name: name.clone(), photo_ids: photo_ids.clone(), cover_photo_id: photo_ids.first().cloned(), - created_at: Utc::now(), + created_at: spacedrive_sdk::clock::now(), album_type: AlbumType::Manual, })?, }], diff --git a/extensions/photos/src/agent/handlers.rs b/extensions/photos/src/agent/handlers.rs index b60fc0c34..dc3ec52df 100644 --- a/extensions/photos/src/agent/handlers.rs +++ b/extensions/photos/src/agent/handlers.rs @@ -21,10 +21,10 @@ impl crate::Photos { #[on_event(EntryCreated)] #[filter(".extension().is_image()")] - pub async fn on_new_photo(entry: Entry, ctx: &AgentContext) -> AgentResult<()> { - ctx.trace(format!("New photo detected: {}", entry.name())); + pub async fn on_new_photo(record: Record, ctx: &AgentContext) -> AgentResult<()> { + ctx.trace(format!("New photo detected: {}", record.name())); - if !ctx.in_granted_scope(&entry.path()) { + if !ctx.in_granted_scope(record.path()) { ctx.trace("Photo not in granted scope - skipping"); return Ok(()); } @@ -33,7 +33,7 @@ impl crate::Photos { memory .plan .update(|mut plan| { - plan.photos_needing_faces.push(entry.id()); + plan.photos_needing_faces.push(record.id()); Ok(plan) }) .await?; diff --git a/extensions/photos/src/jobs/analyze.rs b/extensions/photos/src/jobs/analyze.rs index e72201492..85438fa12 100644 --- a/extensions/photos/src/jobs/analyze.rs +++ b/extensions/photos/src/jobs/analyze.rs @@ -8,37 +8,61 @@ use crate::jobs::clustering::{cluster_faces_into_people, generate_face_tags}; use crate::tasks::*; #[derive(Serialize, Deserialize, Default)] +#[serde(default)] pub struct AnalyzePhotosState { pub photo_ids: Vec, pub current_index: usize, + /// Photos whose faces sidecar was written by this run. + pub analyzed: usize, + /// Photos skipped because they have no content identity yet or the face + /// detector is not available on this device. + pub skipped: usize, } +/// Detect faces in each photo, cluster them into people, and tag the photos. +/// +/// A photo that already has a faces sidecar is left alone, so the job is +/// safe to re-run over a growing set. When the host has no face detection +/// provider the detection step is skipped with one warning and the photo +/// keeps no sidecar, so a later run with a provider picks it up. #[job(name = "analyze_photos")] pub async fn analyze_photos_batch(ctx: &JobContext, state: &mut AnalyzePhotosState) -> Result<()> { ctx.progress(Progress::indeterminate("Analyzing photos for faces...")); let photo_ids = state.photo_ids.clone(); let total = photo_ids.len(); + let mut detector_missing = false; - for (idx, photo_id) in photo_ids.iter().enumerate() { - let photo = ctx.vdfs().get_entry(*photo_id).await?; + for (idx, photo_id) in photo_ids.iter().enumerate().skip(state.current_index) { + let photo = ctx.vdfs().get_record(*photo_id).await?; if let Some(content_uuid) = photo.content_uuid() { - if ctx.sidecar_exists(content_uuid, "faces")? { - continue; + if !ctx.sidecar_exists(content_uuid, "faces")? { + match ctx.run(detect_faces_in_photo, photo.clone()).await { + Ok(faces) => { + ctx.save_sidecar(content_uuid, "faces", &faces).await?; + state.analyzed += 1; + } + Err(Error::NotAvailable(reason)) => { + if !detector_missing { + ctx.add_warning(&format!("face detection skipped: {reason}")); + detector_missing = true; + } + state.skipped += 1; + } + Err(e) => return Err(e), + } } } else { - continue; - } - - let faces = ctx.run(detect_faces_in_photo, photo.clone()).await?; - - if let Some(content_uuid) = photo.content_uuid() { - ctx.save_sidecar(content_uuid, "faces", "photos", &faces) - .await?; + state.skipped += 1; } + state.current_index = idx + 1; + ctx.increment_items(1); ctx.check_interrupt().await?; + if state.current_index % 10 == 0 { + ctx.checkpoint(state)?; + } ctx.progress(Progress::simple( (idx + 1) as f32 / total as f32, diff --git a/extensions/photos/src/jobs/clustering.rs b/extensions/photos/src/jobs/clustering.rs index d616d4e35..210df26b5 100644 --- a/extensions/photos/src/jobs/clustering.rs +++ b/extensions/photos/src/jobs/clustering.rs @@ -1,4 +1,4 @@ -use spacedrive_sdk::{job, task}; +use spacedrive_sdk::task; use spacedrive_sdk::prelude::*; use spacedrive_sdk::tasks::TaskContext; @@ -12,7 +12,7 @@ pub async fn cluster_faces_into_people(ctx: TaskContext, photo_ids: Vec) - let mut all_faces: Vec<(Uuid, FaceDetection)> = Vec::new(); for photo_id in &photo_ids { - let photo = ctx.vdfs().get_entry(*photo_id).await?; + let photo = ctx.vdfs().get_record(*photo_id).await?; if let Some(content_uuid) = photo.content_uuid() { if let Ok(faces) = ctx .read_sidecar::>(content_uuid, "faces") @@ -47,14 +47,14 @@ pub async fn cluster_faces_into_people(ctx: TaskContext, photo_ids: Vec) - #[task] pub async fn generate_face_tags(ctx: TaskContext, photo_ids: Vec) -> TaskResult<()> { for photo_id in &photo_ids { - let photo = ctx.vdfs().get_entry(*photo_id).await?; + let photo = ctx.vdfs().get_record(*photo_id).await?; if let Ok(people) = photo.custom_field::>("identified_people") { for person_id in people { if let Ok(person) = ctx.vdfs().get_model::(person_id).await { if let Some(name) = person.name { ctx.vdfs() - .add_tag(photo.metadata_id(), &format!("#person:{}", name)) + .add_tag(photo.id(), &format!("#person:{}", name)) .await?; } } @@ -65,6 +65,60 @@ pub async fn generate_face_tags(ctx: TaskContext, photo_ids: Vec) -> TaskR Ok(()) } +/// The person whose representative embedding is nearest the cluster's +/// centroid, when it is within the clustering threshold; otherwise a new, +/// unnamed person built from the cluster. async fn find_or_create_person(ctx: &TaskContext, cluster: &FaceCluster) -> TaskResult { - todo!("Implement person matching") + let threshold = ctx + .config::() + .face_clustering_threshold; + let people = ctx.vdfs().query_models::().collect().await?; + let nearest = people + .iter() + .filter(|person| !person.representative_embedding.is_empty()) + .map(|person| { + ( + person, + cosine_distance( + &person.representative_embedding, + &cluster.centroid_embedding, + ), + ) + }) + .filter(|(_, distance)| *distance <= threshold) + .min_by(|a, b| a.1.total_cmp(&b.1)); + + if let Some((person, _)) = nearest { + let embeddings: Vec> = cluster + .faces + .iter() + .map(|(_, face)| face.embedding.clone()) + .collect(); + let photos = cluster.faces.len(); + ctx.vdfs() + .update_model(person.id, |mut p: Person| { + p.embeddings.extend(embeddings); + p.photo_count += photos; + p.representative_embedding = mean_embedding(&p.embeddings); + Ok(p) + }) + .await?; + return Ok(person.id); + } + + let person = Person { + id: Uuid::new_v4(), + name: None, + thumbnail_photo_id: cluster.faces.first().map(|(photo, _)| *photo), + embeddings: cluster + .faces + .iter() + .map(|(_, face)| face.embedding.clone()) + .collect(), + photo_count: cluster.faces.len(), + representative_embedding: cluster.centroid_embedding.clone(), + }; + let id = person.id; + ctx.vdfs().create_model(person).await?; + Ok(id) } diff --git a/extensions/photos/src/jobs/moments.rs b/extensions/photos/src/jobs/moments.rs index 7cad36b49..000cc2155 100644 --- a/extensions/photos/src/jobs/moments.rs +++ b/extensions/photos/src/jobs/moments.rs @@ -1,6 +1,5 @@ use spacedrive_sdk::job; -use chrono::Utc; use serde::{Deserialize, Serialize}; use spacedrive_sdk::prelude::*; use spacedrive_sdk::types::JobResult; diff --git a/extensions/photos/src/jobs/places.rs b/extensions/photos/src/jobs/places.rs index 5224cd302..b4e739430 100644 --- a/extensions/photos/src/jobs/places.rs +++ b/extensions/photos/src/jobs/places.rs @@ -22,7 +22,7 @@ pub async fn identify_places_in_location( let photos = ctx .vdfs() - .query_entries() + .query_records() .in_location(location) .of_type::() .where_metadata("exif.gps", is_not_null()) @@ -50,7 +50,7 @@ pub async fn identify_places_in_location( .await?; ctx.vdfs() - .add_tag(photo.metadata_id(), &format!("#place:{}", place.name)) + .add_tag(photo.id(), &format!("#place:{}", place.name)) .await?; } } diff --git a/extensions/photos/src/jobs/scenes.rs b/extensions/photos/src/jobs/scenes.rs index d0d52ed61..9fc800966 100644 --- a/extensions/photos/src/jobs/scenes.rs +++ b/extensions/photos/src/jobs/scenes.rs @@ -16,13 +16,12 @@ pub struct AnalyzeScenesState { #[job(name = "analyze_scenes")] pub async fn analyze_scenes(ctx: &JobContext, state: &mut AnalyzeScenesState) -> JobResult<()> { for photo_id in &state.photo_ids { - let photo = ctx.vdfs().get_entry(*photo_id).await?; + let photo = ctx.vdfs().get_record(*photo_id).await?; let scenes = ctx.run(classify_scene, photo.clone()).await?; if let Some(content_uuid) = photo.content_uuid() { - ctx.save_sidecar(content_uuid, "scene", "photos", &scenes) - .await?; + ctx.save_sidecar(content_uuid, "scene", &scenes).await?; } for scene in &scenes { @@ -32,7 +31,7 @@ pub async fn analyze_scenes(ctx: &JobContext, state: &mut AnalyzeScenesState) -> .scene_confidence_threshold { ctx.vdfs() - .add_tag(photo.metadata_id(), &format!("#scene:{}", scene.label)) + .add_tag(photo.id(), &format!("#scene:{}", scene.label)) .await?; } } diff --git a/extensions/photos/src/lib.rs b/extensions/photos/src/lib.rs index babbe0021..e183ee421 100644 --- a/extensions/photos/src/lib.rs +++ b/extensions/photos/src/lib.rs @@ -32,6 +32,7 @@ use spacedrive_sdk::{extension, prelude::*}; name = "Photos", version = "1.0.0", jobs = [analyze_photos_batch, create_moments, identify_places_in_location, analyze_scenes], + models = [Person, Place, Moment, Album, Photo], description = "Advanced photo management with faces, places, and intelligent organization", min_core_version = "2.0.0", required_features = ["exif_extraction", "ai_models"], diff --git a/extensions/photos/src/models/photo.rs b/extensions/photos/src/models/photo.rs index dff2a996a..547cbe807 100644 --- a/extensions/photos/src/models/photo.rs +++ b/extensions/photos/src/models/photo.rs @@ -13,7 +13,7 @@ pub struct Photo { pub id: Uuid, #[entry(filter = "*.{jpg,jpeg,png,heic,heif,raw,cr2,nef,dng}")] - pub file: Entry, + pub file: Record, #[metadata] pub exif: Option, @@ -47,10 +47,10 @@ pub struct Photo { } impl Photo { - pub fn from_entry(entry: Entry) -> Self { + pub fn from_record(record: Record) -> Self { Self { - id: entry.id(), - file: entry.clone(), + id: record.id(), + file: record.clone(), exif: None, detected_faces: None, scene_tags: None, diff --git a/extensions/photos/src/queries/search_scene.rs b/extensions/photos/src/queries/search_scene.rs index d4101d77b..3b10d83a7 100644 --- a/extensions/photos/src/queries/search_scene.rs +++ b/extensions/photos/src/queries/search_scene.rs @@ -10,11 +10,12 @@ pub async fn search_scene( ctx: &QueryContext, scene_type: String, ) -> QueryResult> { - ctx.vdfs() - .query_entries() + let records = ctx + .vdfs() + .query_records() .with_tag(&format!("#scene:{}", scene_type)) .of_type::() - .map(|entry| Photo::from_entry(entry)) .collect() - .await + .await?; + Ok(records.into_iter().map(Photo::from_record).collect()) } diff --git a/extensions/photos/src/tasks/classify_scene.rs b/extensions/photos/src/tasks/classify_scene.rs index 1bc5b8c94..840a93b56 100644 --- a/extensions/photos/src/tasks/classify_scene.rs +++ b/extensions/photos/src/tasks/classify_scene.rs @@ -5,7 +5,7 @@ use spacedrive_sdk::prelude::*; use crate::models::SceneTag; #[task(requires_capability = "gpu_optional")] -pub async fn classify_scene(ctx: TaskContext, photo: Entry) -> TaskResult> { +pub async fn classify_scene(ctx: TaskContext, photo: Record) -> TaskResult> { let image_bytes = photo.read().await?; let classifications = ctx diff --git a/extensions/photos/src/tasks/detect_faces.rs b/extensions/photos/src/tasks/detect_faces.rs index e2c8f277a..4e380fa59 100644 --- a/extensions/photos/src/tasks/detect_faces.rs +++ b/extensions/photos/src/tasks/detect_faces.rs @@ -7,7 +7,7 @@ use crate::models::{BoundingBox, FaceDetection}; #[task(retries = 2, timeout_ms = 30000, requires_capability = "gpu_optional")] pub async fn detect_faces_in_photo( ctx: TaskContext, - photo: Entry, + photo: Record, ) -> TaskResult> { let image_bytes = photo.read().await?; diff --git a/extensions/photos/src/utils/clustering.rs b/extensions/photos/src/utils/clustering.rs index 86d4fd532..798d95de5 100644 --- a/extensions/photos/src/utils/clustering.rs +++ b/extensions/photos/src/utils/clustering.rs @@ -4,11 +4,101 @@ use uuid::Uuid; use crate::agent::PhotoEvent; use crate::models::{FaceDetection, *}; -pub fn dbscan_clustering(faces: &[(Uuid, FaceDetection)], threshold: f32) -> Vec { - todo!("Implement DBSCAN clustering") +/// Cosine distance between two embeddings: 0 for identical directions, 1 +/// for orthogonal ones. Vectors of different lengths or zero length are as +/// far apart as possible. +pub fn cosine_distance(a: &[f32], b: &[f32]) -> f32 { + if a.len() != b.len() || a.is_empty() { + return 1.0; + } + let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum(); + let norm_a = a.iter().map(|x| x * x).sum::().sqrt(); + let norm_b = b.iter().map(|x| x * x).sum::().sqrt(); + if norm_a == 0.0 || norm_b == 0.0 { + return 1.0; + } + 1.0 - dot / (norm_a * norm_b) } -pub fn cluster_by_location(photos: &[Entry], radius_meters: f32) -> Vec { +/// The element-wise mean of a set of embeddings; empty when the set is. +pub fn mean_embedding(embeddings: &[Vec]) -> Vec { + let Some(first) = embeddings.first() else { + return Vec::new(); + }; + let mut mean = vec![0.0f32; first.len()]; + let mut count = 0usize; + for embedding in embeddings.iter().filter(|e| e.len() == first.len()) { + for (m, x) in mean.iter_mut().zip(embedding) { + *m += x; + } + count += 1; + } + for m in &mut mean { + *m /= count as f32; + } + mean +} + +/// DBSCAN over face embeddings with cosine distance, `min_points` of 2: +/// a face with no neighbour within `threshold` is noise and belongs to no +/// person. Quadratic in the number of faces, which is fine for one job's +/// batch and avoids an index the guest has no room for. +pub fn dbscan_clustering(faces: &[(Uuid, FaceDetection)], threshold: f32) -> Vec { + const MIN_POINTS: usize = 2; + let n = faces.len(); + let neighbours: Vec> = (0..n) + .map(|i| { + (0..n) + .filter(|&j| { + j != i + && cosine_distance(&faces[i].1.embedding, &faces[j].1.embedding) + <= threshold + }) + .collect() + }) + .collect(); + + let mut cluster_of = vec![None::; n]; + let mut clusters: Vec> = Vec::new(); + for seed in 0..n { + if cluster_of[seed].is_some() || neighbours[seed].len() + 1 < MIN_POINTS { + continue; + } + let id = clusters.len(); + let mut members = Vec::new(); + let mut frontier = vec![seed]; + cluster_of[seed] = Some(id); + while let Some(i) = frontier.pop() { + members.push(i); + if neighbours[i].len() + 1 < MIN_POINTS { + continue; + } + for &j in &neighbours[i] { + if cluster_of[j].is_none() { + cluster_of[j] = Some(id); + frontier.push(j); + } + } + } + clusters.push(members); + } + + clusters + .into_iter() + .map(|members| { + let faces: Vec<(Uuid, FaceDetection)> = + members.iter().map(|&i| faces[i].clone()).collect(); + let embeddings: Vec> = + faces.iter().map(|(_, f)| f.embedding.clone()).collect(); + FaceCluster { + centroid_embedding: mean_embedding(&embeddings), + faces, + } + }) + .collect() +} + +pub fn cluster_by_location(photos: &[Record], radius_meters: f32) -> Vec { todo!("Implement geographic clustering") } @@ -22,6 +112,6 @@ pub struct FaceCluster { } pub struct PlaceCluster { - pub photos: Vec, + pub photos: Vec, pub center: GpsCoordinates, } diff --git a/extensions/test-extension/.cargo/config.toml b/extensions/test-extension/.cargo/config.toml index 5155be886..4dad2754b 100644 --- a/extensions/test-extension/.cargo/config.toml +++ b/extensions/test-extension/.cargo/config.toml @@ -2,5 +2,6 @@ [build] target = "wasm32-unknown-unknown" +# The guest has no entropy of its own; the SDK routes getrandom to the host. [target.wasm32-unknown-unknown] -rustflags = ["-C", "link-arg=--export-table"] +rustflags = ["-C", "link-arg=--export-table", "--cfg", "getrandom_backend=\"custom\""] diff --git a/extensions/test-extension/Cargo.lock b/extensions/test-extension/Cargo.lock index c10ea93c4..f025e1103 100644 --- a/extensions/test-extension/Cargo.lock +++ b/extensions/test-extension/Cargo.lock @@ -8,6 +8,12 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + [[package]] name = "chrono" version = "0.4.45" @@ -18,12 +24,29 @@ dependencies = [ "serde", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + [[package]] name = "memchr" version = "2.7.6" @@ -57,6 +80,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "ryu" version = "1.0.20" @@ -111,6 +140,7 @@ name = "spacedrive-sdk" version = "0.1.0" dependencies = [ "chrono", + "getrandom", "serde", "serde_json", "spacedrive-sdk-macros", @@ -142,9 +172,11 @@ dependencies = [ name = "test-extension" version = "0.1.0" dependencies = [ + "chrono", "serde", "serde_json", "spacedrive-sdk", + "uuid", ] [[package]] @@ -179,5 +211,16 @@ version = "1.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" dependencies = [ + "getrandom", "serde_core", + "uuid-rng-internal", +] + +[[package]] +name = "uuid-rng-internal" +version = "1.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41792896a8681503d839b89f31aa4b0d6f752b0054ae86bab412ea09dcb9fc6c" +dependencies = [ + "getrandom", ] diff --git a/extensions/test-extension/Cargo.toml b/extensions/test-extension/Cargo.toml index 1ca732b95..a7600f497 100644 --- a/extensions/test-extension/Cargo.toml +++ b/extensions/test-extension/Cargo.toml @@ -13,6 +13,8 @@ crate-type = ["cdylib"] serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } spacedrive-sdk = { path = "../../crates/sdk" } +chrono = { version = "0.4", default-features = false, features = ["serde"] } +uuid = { version = "1.0", default-features = false, features = ["serde", "v4", "rng-getrandom"] } [profile.release] codegen-units = 1 diff --git a/extensions/test-extension/manifest.json b/extensions/test-extension/manifest.json index 3791fc9c8..b5f548b3a 100644 --- a/extensions/test-extension/manifest.json +++ b/extensions/test-extension/manifest.json @@ -7,18 +7,9 @@ "homepage": "https://spacedrive.com", "wasm_file": "test_extension.wasm", "permissions": { - "methods": [ - "query:", - "action:" - ], - "libraries": [ - "*" - ], - "rate_limits": { - "requests_per_minute": 1000, - "concurrent_jobs": 10 - }, - "network_access": [], - "max_memory_mb": 256 + "read_records": {}, + "write_sidecars": [ + "digest" + ] } -} \ No newline at end of file +} diff --git a/extensions/test-extension/src/lib.rs b/extensions/test-extension/src/lib.rs index 349cc9922..e4761e174 100644 --- a/extensions/test-extension/src/lib.rs +++ b/extensions/test-extension/src/lib.rs @@ -4,7 +4,7 @@ //! extension development by abstracting FFI and state management details. use spacedrive_sdk::prelude::*; -use spacedrive_sdk::{extension, job}; +use spacedrive_sdk::{extension, job, model, task}; // Extension Definition // The #[extension] macro generates plugin_init() and plugin_cleanup(). @@ -14,7 +14,8 @@ use spacedrive_sdk::{extension, job}; id = "test-extension", name = "Test Extension", version = "0.1.0", - jobs = [test_counter], + jobs = [test_counter, catalog_files], + models = [CatalogEntry], )] struct TestExtension; @@ -74,3 +75,114 @@ fn test_counter(ctx: &JobContext, state: &mut CounterState) -> Result<()> { Ok(()) } + +// Catalog Job +// Exercises the data host functions: records, bytes, sidecars, models, the +// clock and entropy. Reads every file of the given extensions in the +// library, writes a `digest` sidecar per content and a CatalogEntry model +// per file, and skips files already digested so a second run is a no-op. + +/// One catalogued file, a standalone model keyed by its own id. +#[derive(Serialize, Deserialize, Clone)] +#[model(version = "1.0.0")] +pub struct CatalogEntry { + pub id: Uuid, + pub record: Uuid, + pub name: String, + pub size: u64, + pub first_bytes: Vec, + pub catalogued_at: chrono::DateTime, + pub seen_twice: bool, +} + +/// What the `digest` sidecar holds for one content identity. +#[derive(Serialize, Deserialize)] +pub struct Digest { + pub size: usize, + pub first_bytes: Vec, + pub record_name: String, +} + +#[derive(Serialize, Deserialize, Default)] +#[serde(default)] +pub struct CatalogState { + pub extensions: Vec, + pub digested: usize, + pub skipped: usize, + pub entries: usize, +} + +/// Read one record's bytes and describe them. The timeout is generous; the +/// retry covers a transient read failure. +#[task(retries = 1, timeout_ms = 10000)] +async fn digest_record(ctx: TaskContext, record: Record) -> TaskResult { + let bytes = record.read().await?; + let _ = ctx.task_id(); + Ok(Digest { + size: bytes.len(), + first_bytes: bytes.iter().take(4).copied().collect(), + record_name: record.name().to_string(), + }) +} + +#[job(name = "catalog")] +async fn catalog_files(ctx: &JobContext, state: &mut CatalogState) -> Result<()> { + let records = ctx + .vdfs() + .query_records() + .with_extensions(state.extensions.iter().cloned()) + .collect() + .await?; + ctx.log(&format!("Cataloguing {} records", records.len())); + + for record in records { + let Some(content_uuid) = record.content_uuid() else { + ctx.add_warning(&format!("{} has no content identity yet", record.name())); + state.skipped += 1; + continue; + }; + if ctx.sidecar_exists(content_uuid, "digest")? { + let existing: Digest = ctx.read_sidecar(content_uuid, "digest").await?; + ctx.log(&format!( + "{} already digested ({} bytes)", + record.name(), + existing.size + )); + state.skipped += 1; + continue; + } + let digest = ctx.run(digest_record, record.clone()).await?; + ctx.save_sidecar(content_uuid, "digest", &digest).await?; + state.digested += 1; + + let entry = CatalogEntry { + id: Uuid::new_v4(), + record: record.id(), + name: record.name().to_string(), + size: digest.size as u64, + first_bytes: digest.first_bytes.clone(), + catalogued_at: spacedrive_sdk::clock::now(), + seen_twice: false, + }; + let id = entry.id; + ctx.vdfs().create_model(entry).await?; + ctx.vdfs() + .update_model(id, |mut e: CatalogEntry| { + e.seen_twice = true; + Ok(e) + }) + .await?; + state.entries += 1; + ctx.increment_items(1); + } + + let all = ctx.vdfs().query_models::().collect().await?; + ctx.log(&format!( + "Catalog holds {} entries, {} digested this run, {} skipped", + all.len(), + state.digested, + state.skipped + )); + ctx.checkpoint(state)?; + Ok(()) +} diff --git a/extensions/test-extension/test_extension.wasm b/extensions/test-extension/test_extension.wasm index 91516cb45329699f1e2b575f2c071e63965b33c7..998bb3881925354fcef8f97aa4d7112fa6a9a01a 100755 GIT binary patch literal 192176 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 delta 28079 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