A filesystem-based memory layer for agents, with hybrid search, backed by SurrealDB.
Warning
SurrealFS is in beta. The API, schema, and CLI may change in breaking ways between releases, and it is not yet recommended for production use. Bug reports and feedback are very welcome.
Give an agent somewhere durable to keep its work. SurrealFS is a file table
plus the tools to hand it to a model: files and folders, full-text and semantic
search, all queryable with SurrealQL because it is just a table.
The file browser is a standalone tool that can be used to browse and edit files in a SurrealDB database.
- Python 3.12+
- A SurrealDB 3.x server.
pip install surrealfs # core, and the Hermes plugin
pip install "surrealfs[pydantic-ai]" # + the pydantic-ai toolset
pip install "surrealfs[mcp]" # + the MCP server
pip install "surrealfs[browser]" # + the file browserStart a database (just db), then:
from surrealdb import AsyncSurreal
from surrealfs import SurrealFs, apply_schema
db = AsyncSurreal("ws://localhost:8000/rpc")
await db.signin({"username": "root", "password": "root"})
await db.use("surrealfs", "demo")
await apply_schema(db) # defines the `file` table; safe to re-run
fs = SurrealFs(db, user="alice") # who you are; ROOT bypasses permissions
await fs.write_text("/notes/today.md", "# Today\n- ship the refactor")
await fs.edit("/notes/today.md", "ship", "shipped")
await fs.ls("/notes")
await fs.search_text("refactor")You create and own the connection. SurrealFS never connects, signs in, or selects a namespace.
user is required and has no default. Files carry a unix owner and mode, and
the database cannot tell SurrealFS who is asking (every credential is a system
credential), so the identity comes from you. See Permissions.
SurrealFs returns structured FileEntry and SearchHit objects, not
pre-formatted strings. Build whatever integration you like on top.
| Read | read_text read_bytes tail ls glob stat exists |
| Write | write_text write_bytes edit touch mkdir |
| Organise | mv cp rm chmod |
| Search | search search_text search_semantic reindex_embeddings |
Six ways to hand it to an agent, each with its own README:
| pydantic-ai | a FunctionToolset, with recoverable mistakes raised as ModelRetry |
surrealfs[pydantic-ai] |
| Raw JSON tool schemas | plain dicts in Anthropic or OpenAI shape, no framework | core |
| Hermes | the 14 surrealfs_* tools plus a bundled notes skill |
core |
| Hermes memory | files every completed turn, and recalls context before each one | core |
| MCP | the 16 tools over stdio, for Claude, Cursor, Zed, Codex or your own | surrealfs[mcp] |
| Claude | that server as a plugin, bundled with the /brain skill |
surrealfs[mcp,agent-memory] |
The four tool surfaces are generated from one registry in
surrealfs/tools/, so they cannot drift apart. Tool descriptions are markdown in
surrealfs/tools/docs/. Edit them as prose; they are prompt text. The memory
provider is the odd one out: it exposes no tools of its own, because the Hermes
plugin's already cover explicit reads and writes.
Point the agents at a shared SurrealDB (a Cloud instance, say) and every person on the team can run the browser on their own machine to see and edit the same filesystem the agents are writing to:
pip install "surrealfs[browser]"
surrealfs-browser # http://127.0.0.1:7933Tree on the left, file on the right, chat with the note-taking agent on the far
right. Text is editable and saves back to the table, markdown renders with a
source toggle, agent-authored HTML renders in a sandboxed iframe, images display,
and the search box is the same hybrid fs.search the agent's own tool calls.
Conversations are files too, under /_sessions/: open one from the tree and it
replays.
Credentials come from a .env in the working directory, or the environment
directly, which wins over the file:
SURREALDB_URL=wss://….surreal.cloud/rpc
SURREALDB_USER=…
SURREALDB_PASS=…
SURREALDB_NAMESPACE=…
SURREALDB_DATABASE=…
SURREALDB_AUTH_LEVEL=database # root (default) | namespace | database
OPENAI_API_KEY=… # optional: makes search hybrid
ANTHROPIC_API_KEY=… # optional: enables the chat panelSURREALDB_AUTH_LEVEL picks which kind of user the credentials are, since the
server infers that from the signin payload: a database-scoped Cloud credential
needs database. All three are system users, which is why SurrealFS enforces
permissions itself rather than leaning on the database; SURREALFS_USER (or
--user) picks who the browser acts as, defaulting to root, which sees every
private home. Every variable has a command-line form too
(surrealfs-browser --help).
The server binds loopback. --host 0.0.0.0 exposes it, and then anyone who can
reach the port has whatever access those credentials do. The page has no login
of its own.
The page is a React app built on the SurrealDB UI Kit. Its source is
in surrealfs/browser/ui/, and the build output it serves is gitignored, so
working on this repo needs bun:
just ui # build the page into surrealfs/browser/static
just ui-dev # vite dev server, proxying the API to `just browser`just browser builds it for you. Nothing else in the project needs bun --
just check stays Python-only.
fs.search runs a full-text arm and a vector arm and fuses them by rank.
SurrealFS never calls an embedding model: you pass the embedder, so you pick
the provider, and without one search stays full-text only. An OpenAI embedder
and a daemon that keeps the vectors current ship in surrealfs.embed:
just embed # poll every 5s; --once for a single passThe queries are custom SurrealQL functions in the schema, not Python, so any client gets the same ranked, permission-filtered retrieval:
SELECT path, rrf_score FROM fn::sfs_hybrid_search("how do I get paid", $qvec, 5, NONE, 'alice');fn::sfs_search_text and fn::sfs_search_semantic are the single arms. The last
argument is who is asking: a system credential passes it, a record credential
overrides it. SurrealFs calls these too, so there is only one definition.
See Semantic search for the ranking, match="all",
the SurrealQL surface, and wiring it into the agent toolset.
Files carry a unix owner and mode, /home/<user> is private, everything
else is shared, and chmod moves things between the two. SurrealFs enforces
it. See Permissions for the rules and the reasoning.
The above is a real boundary for an agent, since no tool exposes raw SurrealQL, but not one against anyone holding the database credential. If several agents share a database, opt in:
python -m surrealfs.schema --record-auth # adds the `user` table + record access
python -m surrealfs.users add alice # creates alice and /home/alice (0700)SURREALDB_AUTH_LEVEL=record SURREALDB_USER=alice SURREALDB_PASS=…Now SurrealDB itself refuses to return or modify anything in another user's
home, whatever query it is asked. Nothing else changes: the tools, the modes
and chmod behave the same, and SurrealFs picks its identity up from the
credential instead of the environment. Deployments that do not opt in are
unaffected: the permissions clause is inert for system credentials.
surrealfs/schema/file.surql is the whole data model, and it is worth reading.
The key idea: a file's parent is a record link and path is a computed
field derived from the parent chain. Nothing stores a path, so renaming or
moving a folder is a single UPDATE and every descendant's path follows for
free.
DEFINE FIELD path ON file COMPUTED (
($this.parent.path || '') + '/' + <string>$this.filename
) TYPE string;A folder is just a row with no content, no file bytes, and no symlink,
also computed, as is_folder. hash is maintained by an event, and there are
HNSW and BM25 indexes for the two search modes.
owner, mode and the computed gate are the permission model. See
Permissions below and docs/permissions.md.
You can apply it without writing any code:
python -m surrealfs.schema # uses SURREALDB_* env vars
python -m surrealfs.schema --print # dump the DDL, connect to nothingjust db # start SurrealDB in one terminal
just schema # apply the file table to it
just test # run the suite (starts its own server if needed)
just check # lint + test
just browser # the file browser on :7933
just agent # the note-taking chat agent on :7932
just loop "…" # the framework-free Anthropic tool-use loop
just mcp # the MCP server over stdio, as Claude Desktop launches itTests start a throwaway surreal server; set SURREALFS_TEST_URL to reuse one
you already have.
