YmerNode (Ymer Node v0.5.0)

Copy Markdown View Source

This module is the map — what exists and why. Each entry defends a module's existence; how a module is designed lives in its own @moduledoc.

Ymer Node is the headless local half of one product. It gives an LLM worker capability that has to live on this machine, while the coordination that has to be reachable from anywhere lives in ymer. It has no UI of its own and no work engine, and it never routes work. A client reaches it over MCP at /mcp, on loopback — pinned by the bind when the node runs directly, and by the host publish when it runs in a container (YmerNode.Mcp owns that distinction).

The line against a work engine falls here: on its own, the node does one thing — it runs an accepted script's action on a schedule, with fixed args and nobody there, a watched reference's fetch among them (YmerNode.Schedules). It is a script runner with scheduling flexibility, not a reliable scheduler, and nothing it runs is routed, chained, retried or handed to a model; anything that needs a task, a claim or an LLM is ymer's.

Two properties shape everything below. Local serving never depends on ymer: every tool here answers whether or not the node can reach anything, so an unreachable ymer degrades identity, never capability. And the node owns one precious thing — notebook.db. Everything else it holds is rebuildable, which is why exactly one subsystem has a durability obligation. That is the durability rule, and "rebuildable" is the admission test for anything else the node stores. node.db and the cache directory are what the rule has admitted so far. The node database's schema is this codebase's and migrations run at every boot, so deleting it costs nothing a boot does not put back; the cache directory (YmerNode.References.Cache.dir/0) holds only what cache entries in that database name, all of it refetchable: every boot removes the files no cache entry names and the cache entries whose file is gone, and a cache entry whose file goes between boots is fetched again at its next read — so the directory follows the database, and the two are rebuilt together. Neither is backed up, and "back up the node" goes on meaning the store alone. The files directory a script reads and writes (YmerNode.Script.Context.files_dir/1) sits outside the rule altogether: the node creates it and never touches its contents on its own, so the files there are the user's — neither precious to the node nor rebuildable by it.

flowchart LR
    Mcp[MCP mount]
    Notebook[Notebook]
    Backup[Notebook/Backup]
    References[References]
    Scripts[Scripts]
    Schedules[Schedules]

    NotebookDB[(notebook.db)]
    NodeDB[(node.db)]
    BackupFiles[(backup files)]
    SecretsFile[(secrets.env)]
    FilesDir[(files directory)]
    CacheDir[(cache directory)]
    BrowserService(browser service)

    Mcp -->|"serves the notebook tool"| Notebook
    Mcp -->|"capture and restore"| Backup
    Mcp -->|"serves the references tool"| References
    Mcp -->|"serves the scripts and script_author tools"| Scripts
    Notebook -->|"the lock's held op, to tell a restore's window apart"| Backup
    Notebook --> NotebookDB
    References -->|"declarations of accepted scripts"| Scripts
    Scripts -->|"reserved names, claimed hosts"| References
    References --> NodeDB
    References -->|"runs a recipe to fill the cache"| Scripts
    References -->|"file cache entries"| CacheDir
    Scripts --> NodeDB
    Scripts -->|"a run's batteries"| Notebook
    Scripts --> SecretsFile
    Scripts -->|"a run reads and writes files"| FilesDir
    Scripts -->|"a run's browser calls"| BrowserService
    Mcp -->|"serves the schedules tool"| Schedules
    Schedules -->|"runs an action at each firing"| Scripts
    Scripts -->|"the schedule holding a run in flight"| Schedules
    Schedules --> NodeDB
    Schedules -->|"a watch's firing refreshes an entry"| References
    Backup -->|"VACUUM INTO"| NotebookDB
    Backup --> BackupFiles

The notebook

YmerNode.Notebook is the raw-SQL surface over notebook.db — a store whose schema belongs to the user and the LLM, not to this codebase. It owns no Ecto.Schema and ships no migrations; the agent issues its own DDL. The module is thin by design: the only behaviour beyond pass-through is the safety model (a rollback that makes reads provably read-only, and a block on the verbs that would reach outside this database) and a _meta description layer that gives SQLite the column comments it lacks.

YmerNode.Notebook.Repo exists to give that store a supervised connection pool and to load the vendored sqlite-vec extension on every connection, so vector tables and KNN search are available through the same SQL surface as everything else. YmerNode.Notebook.VecLoadCheck is the boot probe that turns a silently failed extension load into a refusal to start.

Backup

YmerNode.Notebook.Backup is a notebook operation, not a subsystem beside it: the node has exactly one database worth retaining, so "back up" and "back up the notebook" are the same act. That is a statement about what it owns, not about who calls it — the tool layer reaches it directly, which is why the map above draws an edge from the mount and not only through YmerNode.Notebook. The edge the other way is narrow and one-directional: YmerNode.Notebook reads the lock's held op to tell a restore's window — the one stopped state that closes on its own — from every other one, a rule its own moduledoc owns. YmerNode.Notebook.Backup is deliberately a separate, privileged path from the agent's SQL surface — it replaces the store whole, which is not something a prompt-injected agent may reach. YmerNode.Notebook.Backup.Lock serialises capture against restore.

References

YmerNode.References is the registry: the node's set of references, each a pointer naming where knowledge lives and when to look, never what it says. It exists because a worker that re-documents what already lives somewhere has made a second copy to keep in step, while a pointer and the call that follows it stay correct on their own. The rule that keeps a reference a pointer while its content lives in the cache — the membrane — is stated there.

YmerNode.References.Cache is the cache: what each reference's target says, fetched by running the reference's own script — the node still never makes an outbound call — and served by the references tool as a window of text, an image, or a description. It exists so that a reference reads like a local file while staying a pointer: every entry says how old it is, and how far it may lag its target is the membrane's. YmerNode.References.CacheEntry is its row, YmerNode.References.Cache.Window the line window it serves text by, and YmerNode.References.Cache.Reconcile the boot step that keeps the cache directory rebuildable.

YmerNode.References.Sources derives a reference's source and fetch recipe from its uri at read time, against declarations that accepted scripts supply, so what a reference resolves to follows the node's current capabilities rather than whatever was true when the row was written. YmerNode.References.Search is the ranked find over the registry, and owns the mode contract.

YmerNode.Repo is the node database's repo and the exact counterweight to YmerNode.Notebook.Repo: this one owns its schema and ships the migrations that make node.db rebuildable, which is precisely what the notebook's repo must never do.

Scripts

YmerNode.Scripts is the third capability: small Elixir modules the node has accepted, compiled into this VM and run on request. It exists because the node cannot ship a tool for every system a worker needs to reach, and because the systems worth reaching are different on every machine — a script is how this node learns to reach one without a release.

The boundary is which scripts are accepted. A run is isolated from crashes and from loops — its own supervised process, its own deadline, killed when the deadline passes — but it is not sandboxed: an accepted script runs with the node's own permissions, reaches the network and the notebook, and could do anything this process could do. Nothing about the run limits that, so the question the design answers is not what may a script do but which code is this node willing to run. The answer is: exactly the bytes someone accepted, and nothing else.

That is the acceptance invariant, and it is one comparison. A row carries the hash of its code and the hash this node accepted; run refuses unless the two are equal. Code that changed after it was accepted is code nobody accepted. Three doors accept today — an MCP client through script_author, a human through the CLI, and the build, whose example scripts migrations plant on a fresh node database at exactly the bytes the image carries, because whoever built the image read them — and each is a deliberate act by someone who can read what they are landing. A fourth, registry sync, is why the unaccepted state exists at all before any door can produce it.

YmerNode.Scripts.Script is the row: the code, the two hashes, and the description and declarations denormalised from the compiled module so a listing and the references seam are one query each. YmerNode.Scripts.Compiler turns code into modules and refuses, before compiling anything, a module it can see defined outside Script. — a guard against the honest mistake, not a second boundary, because a module body runs while it compiles and the door that decides what compiles at all is the one above. YmerNode.Scripts.Loader owns every compile and purge in one process, and holds what this boot knows about each script. YmerNode.Scripts.Runner is one run: the acceptance check, the argument check, the deadline, and the shape of every answer. YmerNode.Script is the contract a script implements and YmerNode.Script.Context the batteries it is handed — Req, the notebook, secrets, throttles, the node's time zone, the files directory, the Typst renders and browser calls — which is why a script needs no dependencies of its own. YmerNode.Scripts.Throttle is the one battery that is a process: one per throttle name, shared by every script and run naming it, holding the bucket and the breaker the account behind the name needs. YmerNode.Scripts.Guide renders the two, at the call, as the text scripts guide answers: the contract has one home, a client reads the same bytes a developer reads on the published docs, and the guide ends with the one thing only the running node can say — the applications its release carries. YmerNode.Scripts.PackageDocs renders a promised package's own docs the same way, at the version the release carries, as scripts info <name> answers: the guide stays a map from job to package, and the node authors promises and what a row cannot say, never library teaching.

YmerNode.Secrets keeps the values a script resolves by name, in a file beside the databases rather than in one. That is the durability rule holding: a secrets table would make one row of node.db precious and the rule would have to grow an exception, where a file leaves it intact and costs a re-set rather than a restore.

YmerNode.Scripts.BrowserService is the node's half of the browser service, a program the user runs on their own machine that drives a real browser for a script's browser calls. It owns where the service is, the token it sends there, how long a call may wait and how a failure reads, and nothing of the browser itself: the Chromium builds alone are hundreds of megabytes that most nodes never use, and Playwright's releases would become the node's. So the service stays outside the image, and a node without one still answers every call — with a refusal naming what to run.

YmerNode.Scripts.CLI is the operator's door, run on the machine itself — where importing a file from a repository and setting a secret that must never pass through a model's context both belong.

YmerNode.Script.Harness and YmerNode.Script.Test are for a script developed outside the node, in a repository that takes ymer_node under runtime: false and so never starts its application. The harness is the runtime-safe half — the run tree, YmerNode.Script.Harness.run_tree/0, and setters for the three keys such a VM needs — and YmerNode.Script.Test the ExUnit layer over it. Neither is rendered into scripts guide: they arrange a VM around a script, and a script is handed none of their setters. The guide names one of them only where YmerNode.Script.Context.files_dir/1 refuses an unset directory, as the fix for a VM outside the node.

Schedules

YmerNode.Schedules is how a script runs with nobody there. A schedule is a standing instruction to run one action of one accepted script, with fixed args, on a cron expression in the node's time zone, until its lifetime ends — 90 days at most, because a schedule nobody remembers must not fire for ever. It exists because a report wanted at 07:00, or a check every hour, cannot wait for a session to ask, and one mechanism serves every such need — a watch too, the schedule a reference owns to keep its cache entry current for a few hours. A firing runs through YmerNode.Scripts.run/3, so everything the Scripts section says of a run holds for it. A schedule lives in node.db beside what it runs — its script, or a watch's reference — and is deleted with it: it means nothing without that row, so it shares the row's fate under the durability rule. YmerNode.Schedules.Scheduler is the clock that fires them.

The MCP surface

YmerNode.Mcp is the mount: the node's entire external surface, and the one place its tool list is declared. YmerNode.Mcp.McpServer holds the instructions a client is handed at connect — including the paragraph naming ymer as the other half of the product, which is the only way a client learns the node is not the whole story. YmerNode.Mcp.Tools.Notebook, YmerNode.Mcp.Tools.References, YmerNode.Mcp.Tools.Scripts, YmerNode.Mcp.Tools.ScriptAuthor and YmerNode.Mcp.Tools.Schedules are the tools themselves, each a thin boundary over its own context; YmerNode.Mcp.Tools.Helpers holds the handful of shaping functions their action layers share, and YmerNode.Mcp.Tools.ScriptFormat the rendering the two script tools share.

Running and authoring are two tools rather than one so that a client can be granted the first without the second: what runs is bounded by what has been accepted, and accepting is the decision worth a human's attention. The schedules tool is a third for the same reason: a client allowed to run what has been accepted is not thereby allowed to set it running unattended. The one exception is bounded: references starts a watch — one reference's URL action, for twelve hours at most — and the watch is listed and removable through schedules like any other.