Home/API
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.
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/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 hitsREST + 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 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>/# synafs hit view with path / lines / score headers then the span./.syna/symbol/<name>/{def,refs,callers,callees}/.syna/similar/<path>/.syna/history/<path>/<rev>/.syna/ctlcommit, 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
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.
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 /healthzgeneration (the only route that skips auth).POST /search · /read_span · /symbol · /neighborsconsistency:strong + ryw_token.POST /apply_edit · /commit · /diff · /historyGET /events · /wsgRPC 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).
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.
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.