Home/API

Triple API surface

Reach the same engine three ways.

Read a file, call a syscall, or hit the network — the query semantics, and the results, are identical. On the web tier REST, gRPC and MCP all pass through one App::dispatch, so a Search over gRPC returns exactly what /search does.

NATIVE · FUSE

Filesystem

Mount the repo passthrough (hot reads ~0 overhead) with a virtual /.syna/** namespace. cat, ls, grep, getfattr just work.

$ syna mount /mnt/repo
$ ls /mnt/repo/.syna/\
     symbol/validate_token/callers
SYSCALL · libsyna

Device & C ABI

/dev/synafs (CUSE) with a write-query / read-NDJSON model, plus libsyna.so for any FFI caller. io_uring batch.

echo '{"text":"parse config"}' \
  > /dev/synafs
cat /dev/synafs   # NDJSON hits
WEB · MCP / gRPC / REST

Network

REST + WebSocket, gRPC-Web and native HTTP/2 (hand-rolled HPACK), MCP for agents. One App::dispatch; TLS/mTLS opt-in.

$ syna serve --addr :5200   # REST
$ syna grpc  --addr :5201   # HTTP/2
$ syna-mcp .                # agents
A

Native — FUSE magic paths

A pure-Rust fuser mount (no libfuse — default-features=false, mounted via fusermount3) lays a passthrough over the backing repo: everything outside /.syna/** is byte-for-byte the underlying filesystem, and hot reads are forwarded with near-zero overhead (FUSE passthrough / FOPEN_PASSTHROUGH for direct kernel I/O). Only the /.syna prefix is intercepted; SynaFS logic lives in a pure, unit-tested resolver.

The virtual namespace turns queries into directories you can list, read, and pipe through ordinary tools:

/.syna/query/<q>/
URL-encoded query (filters included) → a directory of hit entries; each entry reads back as a # synafs hit view with path / lines / score headers then the span.
/.syna/symbol/<name>/{def,refs,callers,callees}
Symbol-graph relations as files; read for the symbol's location view.
/.syna/similar/<path>
Semantic neighbours as symlinks back to the real files (target passthrough).
/.syna/history/<path>/<rev>
The as-of blob content of that path at that revision.
/.syna/ctl
Write commands (commit, reindex, sync) to trigger engine actions.

Results are lazy: nothing runs until readdir, and they're cached by (query, generation) on a short TTL so repeated stat storms are cheap. Each backing file also carries virtual xattrs — user.syna.embedding (base64 vector), user.syna.symbols (JSON), user.syna.summary (optional LLM summary) and user.syna.generation — readable with getfattr -d and read-only.

$ syna mount /mnt/repo
# a query is just a directory; ls runs it lazily
$ ls /mnt/repo/.syna/query/parse%20config%3F--lang%3Drust/
000_src_config_rs_L12.view   001_src_cli_rs_L88.view  ...
$ cat /mnt/repo/.syna/query/parse%20config%3F--lang%3Drust/000_src_config_rs_L12.view
# synafs hit  ·  path: src/config.rs  ·  lines: 12-40  ·  score: 0.031
$ getfattr -d -m user.syna /mnt/repo/src/config.rs
B

Syscall — device & C ABI

New syscalls would need a kernel patch, so the engine is exposed as a /dev/synafs CUSE character device (userspace char device — the same wire protocol as FUSE, implemented directly over /dev/cuse, no libfuse). Variable-length JSON moves over write/read instead of the ioctl-retry dance: write(query json) runs the query, read drains the results as NDJSON (one hit per line + a trailing summary line).

Three ioctls, numbered _IOWR('S', nr, …), switch a fd's mode: SYNA_QUERY (1), SYNA_OPEN_SEMANTIC (2, route the next read to the last query's best match), SYNA_SUBSCRIBE (3, turn the fd into a reindex-event NDJSON long-poll, same meaning as the web /events).

For any C / C++ / FFI caller there is libsyna.so with a small, stable JSON-in / JSON-out C ABI (struct marshalling avoided on purpose):

// libsyna.h — JSON in / JSON out
typedef void *syna_handle;

syna_handle syna_open(const char *repo_path);                       // NULL on error
int  syna_query(syna_handle h, const char *json, char **out);       // 0=OK, *out=malloc'd JSON
int  syna_query_batch(syna_handle h, const char *json_arr, char **out); // N queries, one crossing
void syna_free(char *s);                                            // free *out
void syna_close(syna_handle h);

syna_query_batch runs N queries in a single boundary crossing (submit-N / complete-N), isolating per-slot failures as {"error":code}. The kernel-side counterpart is the --features io_uring batch: write_read_batch chains each item's write(query)→read(result) with IOSQE_IO_LINK so a whole batch is submitted and reaped in one io_uring cycle — pure-Rust io-uring (raw io_uring_setup/enter, no liburing). Device creation needs CAP_SYS_ADMIN, but the protocol state machine, dispatch and NDJSON codec are pure functions and are unit-tested without a device.

C

Web — MCP / REST / WS / gRPC

MCP is the agent front door: syna-mcp speaks line-delimited JSON-RPC 2.0 over stdio with tools search, read_span, symbol_lookup, neighbors, apply_edit, subscribe and diff_symbol. Register it with Claude Code or any MCP client (command = syna-mcp, args = [repo]).

REST (tiny_http, synchronous, loopback bind + Bearer token) exposes the same engine over HTTP/JSON:

GET /healthz
Liveness + generation (the only route that skips auth).
POST /search · /read_span · /symbol · /neighbors
Hybrid search, exact spans, symbol relations, semantic neighbours — read-your-writes via consistency:strong + ryw_token.
POST /apply_edit · /commit · /diff · /history
Edit (returns a token), commit, symbol diff and version history.
GET /events · /ws
Reindex events as a long-poll, or a WebSocket (RFC 6455, hand-rolled sha1/base64) text-frame stream.

gRPC comes two ways: gRPC-Web (application/grpc-web+proto) over plain HTTP/1.1, and native HTTP/2 (h2c) with a hand-rolled HPACK (Huffman included) — both with no async runtime and no protoc. Search, ApplyEdit, Commit and Health route through REST's same App::dispatch (protobuf → Value → dispatch → Value → protobuf), so a gRPC Search hit equals the REST /search hit. TLS/mTLS is opt-in via rustls (client certs without a chain to the CA are rejected at the handshake, before dispatch). Ports: 5200 (web) / 5201 (gRPC).

D

Auto-reindex — any surface, always fresh

Because every surface shares one write path, a write from any of them — a FUSE passthrough write, apply_edit over REST/MCP, or an external editor — triggers reindex. syna watch closes the loop with two kernel fsnotify backends: inotify (the default, unprivileged, runs in sandboxes/CI), and fanotify (--fanotify, raw fanotify_init/fanotify_mark syscalls, FAN_MARK_MOUNT to cover a whole mount, needs CAP_SYS_ADMIN). A per-path content-hash guard stops the reindex write-back from echoing, and .syna/dotfiles are skipped.

One engine, identical results. REST, gRPC and MCP go through the same App::dispatch; FUSE, the device, and libsyna run the same syna-query model. A query means the same thing — and returns the same hits — no matter which surface you reach it through.