unifier
Filesystem postbox for inter-process communication. Programs share state by writing values to files in a Unix tree, treating the filesystem as a global data structure — the same strategy used by filesystem-git-issues (directories are namespaces, files are keys, UUID-named files are messages).
No central server is required.
State layout
Default root: ~/.local/unifier (override with $UNIFIER_HOME or --home).
~/.local/unifier/
keys/<path> # persistent key-value (global)
mailbox/<recipient>/<uuid>.txt # point-to-point messages
cron/<min>_<hour>_<dom>_<mon>_<dow>/<uuid>.txt # scheduled drops
chroots/
<name>/
keys/
mailbox/
cron/ # isolated subtree (--chroot)
Cron schedule directories use five underscore-separated fields. Use * for “any” (e.g. 0_9_*_*_* = 09:00 daily).
CLI reference
| Command | Description |
|---|---|
put <key> <value> |
Write a persistent key under keys/ |
get <key> |
Read a key |
del <key> |
Delete a key |
send [--from <agent>] <recipient> <message> |
Drop a mailbox envelope (id, from, to, payload) |
message --from <agent> <recipient> '<json>' |
Structured agent mailbox message + event-socket wakeup |
event '<json>' |
Post a JSON event under events/ and notify listeners |
daemon watch |
Print wakeup notices from .daemon/events.sock (for Jan cron) |
cron <schedule> <message> |
Schedule a cron message |
poll <recipient> [--ack] |
Collect mailbox messages |
poll-cron [--ack] |
Collect messages whose schedule matches now |
list <path> |
List message files under a subtree |
ack <uuid|path> |
Remove a processed message |
root |
Print the effective state root |
daemon start |
Start hot in-memory daemon (background) |
daemon stop |
Stop daemon (flushes dirty state) |
daemon status |
Show whether daemon is running |
daemon flush |
Write dirty in-memory state to disk |
tick start [label] |
Begin ACID tick (reads frozen previous state) |
tick end |
Commit tick to disk with versioning under ticks/<n>/ |
tick status |
Show committed/active tick, queue, and locks |
tick lock <key> / tick unlock <key> |
Lock keys during active tick |
chroot init <name> |
Create chroots/<name>/ with keys/, mailbox/, cron/ |
chroot list |
List chroot names |
Global flags:
--home <path>/$UNIFIER_HOME— top-level store directory--chroot <name>/$UNIFIER_CHROOT— scope data commands tochroots/<name>/--no-daemon— force direct filesystem access even when a hot daemon is running
Examples
# Build
# Shared key-value
# Mailbox messaging
# Agent envelopes + wakeup (Jan listens on events.sock)
# Cron drops
# Chroots
Installation
The binary is unifier. crates.io uses unifier-cli because unifier is already taken.
Releasing
Same flow as jan-cli. From unifier/:
Monorepo aliases:
This bumps Cargo.toml + CHANGELOG.md, commits those version files, runs tests, then cargo publish as unifier-cli.
Development
Summary of changes
Initial implementation
Created the unifier Rust CLI project with:
- Filesystem-as-store model — inspired by
filesystem-git-issues: entities map to directories, scalar values to files, messages to UUID.txtdrops. - State root at
~/.local/unifierwith$UNIFIER_HOME/--homeoverride. - Key-value store —
put,get,delunderkeys/, with atomic writes forput. - Mailbox —
send,poll,ackfor point-to-point messages undermailbox/<recipient>/. - Cron postbox —
cron,poll-cron,listusing five-field schedule directory names matched against the current clock. - Path safety — keys and recipients reject
..; message ack resolves paths relative to the store root. - Tests — unit tests for cron parsing/matching; integration tests for put/get, mailbox, cron, and root.
project.meta.yaml— monorepo metadata, build scripts, and roadmap for future applications (zshrc hooks, agent pipelines, job queues, etc.).
Chroot support
Added isolated subtrees so multiple programs can share one global store without seeing each other's data:
- On-disk layout —
chroots/<name>/contains a full copy of the layout (keys/,mailbox/,cron/). --chroot/$UNIFIER_CHROOT— all data commands operate only inside the named chroot;UnifierHome::path()returns the effective subtree root.- Admin commands —
chroot init <name>creates a new sandbox;chroot listenumerates existing chroots (always uses the global root). - Scope module (
scope.rs) — centralizes path validation:resolve_under_root()rejects..and absolute escapes; used bylistandack. - Isolation tests — verify keys in one chroot are invisible to the global store and other chroots; path escape attempts fail.
- Backward compatibility — omitting
--chrootpreserves the original global-root behavior.
Source layout
src/
main.rs # binary entry point
lib.rs # library + run()
home.rs # UnifierHome (global + effective root)
scope.rs # chroot path confinement
chroot.rs # chroot init/list
paths.rs # path builders for keys, mailbox, cron
postbox.rs # put/get/send/poll/ack operations
cron.rs # schedule parsing and matching
fs_text.rs # read/write helpers
constants.rs # directory name constants
error.rs # Error type
cli/
defs.rs # clap command definitions
mod.rs # dispatch
tests/
cli_integration.rs
Hot daemon
Added an in-memory hot store with on-demand disk flush:
HotStore(store.rs) — loads keys/mailbox/cron from disk; tracks dirty entries;flush()writes only changed data- Daemon process —
unifier daemon start|run|stop|status|flush|watch; control socket at<store>/.daemon/unifier.sock; event output at<store>/.daemon/events.sock - Mailbox envelopes — messages store
{id, from, to, payload}; send/message/event broadcast a wakeup notice (to+id) so Jan can fetchmailbox/<to>/<id>.txt - Auto-routing — data commands use the daemon when running;
--no-daemonforces direct filesystem access - Shutdown flush —
daemon stopand daemon shutdown request flush dirty state before exit
Source layout (updated)
src/
store.rs # HotStore (in-memory + flush)
daemon/ # protocol, client, server, lifecycle
...
Roadmap
See project.meta.yaml for the full list. Next major themes:
- Span-tree logging (
unifier log) and daemon web server - Tick-based agents (sense/decide/act lockstep, concurrency modes, differential sensing)
- Factorio factory model: isolated arms coordinated toward a shared system goal