Skip to content

Repository files navigation

NAVis

A personal AI agent, built from the primitives up.

I spent a weekend building NAVis, a personal always-on AI agent running on a Hetzner VPS with Telegram as the interface. I was inspired by Marc Andreessen's framing that Agent = LLM + Shell + Filesystem + Markdown + Cron. ShMaLLFiCr (Shell, Markdown, LLM, Filesystem, Cron). An LLM to reason, a shell to act, a filesystem to remember, markdown as the format that makes memory legible to both humans and the model, and cron to run it autonomously. A Unix-for-AI composition where every component was already known but their combination unlocks autonomous operation. I wanted to understand this architecture from the inside, not just use it. The question underneath every design decision is the same: what happens when I'm not watching and something goes wrong?

NAVis is built on top of OpenClaw, an open-source agent framework. I did not build OpenClaw. What is mine: NAVis's identity files, the architectural teardown below, my mental model of the gateway's internals grounded in my specific Hetzner + Tailscale setup, the diagrams, the debugging log, and the reading of how ShMaLLFiCr maps to this system.


The design philosophy

You are not building a chatbot. You are building a system you can trust to act in the world without you watching. Every capability in NAVis exists to answer a specific question about unsupervised operation:

  • Security first -- who can reach the agent when you're not there?
  • Identity files -- what does it believe about itself and you when there's no one to correct it?
  • Read before write -- what's the worst it can do if it gets something wrong?
  • Approval gates -- what requires you back in the loop before something irreversible happens?
  • Incremental capability -- have you actually seen it handle the smaller thing correctly before it gets the bigger one?

The through-line: every design decision is an answer to "what happens when I'm not watching and something goes wrong?" The design arc that follows from this: contain it → define it → limit its blast radius → gate its irreversible actions → prove it at each level before expanding scope. NAVis is built in that order.


What NAVis actually does today

Six capabilities, in roughly the order a serious reader should care about them. The architectural ones come first; the email pipeline is a proof point, not the product.

  1. Persistent identity across stateless sessions. Four markdown files (SOUL, USER, AGENTS, MEMORY) load on every turn. The LLM is stateless; the filesystem carries the identity. Constraints beat aspirations: specific prohibitions survive attention dilution, vague positive qualities don't.
  2. A named writer sub-agent with its own SOUL. A second identity file set scoped to long-form writing, called from the main agent with a narrow contract. Same filesystem, different voice, enforced by a different SOUL.
  3. A self-audit across all 10 capability checkpoints. NAVis can inspect its own configuration, report which capabilities are live, and flag drift from the identity files.
  4. An always-on Telegram chat interface. Bot long-poll from the gateway process. Messages land in the agent loop as ordinary user turns.
  5. Live web search via Brave Search API. One outbound tool alongside the LLM API call.
  6. Morning Gmail summary at 08:00 Europe/Berlin, delivered to Telegram. Outbound email with a confirmation gate (NAVis drafts, I approve, NAVis sends). The outbound step is the one that required the most care because it's the first capability that can act on the world without me.

Architecture

Three diagrams, three audiences. Open each HTML file directly in a browser (GitHub renders HTML as source, not as a page).

diagrams/navis-architecture-technical.html is for an AI or infrastructure engineer scrutinising the architecture. Four layers (transport, kernel sockets, gateway process and agent loop, filesystem), three entry paths (webchat, terminal, cron). Corrected against live ss -tlnp output on the running gateway process. This is the one an engineer will judge me on.

diagrams/navis-architecture-lite.html is the same system with fewer boxes. Three entry paths, one agent loop, one filesystem. Meant for a technical PM peer or for me at a glance.

diagrams/navis-story.html is the 30-second version. A person, a server, a model, a phone. No ports, no sockets. For a recruiter or a curious friend.


ShMaLLFiCr in my setup

Andreessen's framing in abstract, and what each component actually is in NAVis.

Component What it is in the abstract What it is in NAVis specifically
Shell The action surface. How the agent affects anything outside its own process. Shell scripts under /usr/local/bin/ (gmail-to-telegram, send-email-confirmed, etc.), invoked as tools from the agent loop. Scripts, not skills, for any step that must execute deterministically.
Markdown The format that makes memory legible to both humans and the model. SOUL.md, USER.md, AGENTS.md, MEMORY.md loaded per turn, plus the writer sub-agent's own SOUL. Constraints before capabilities. Anti-pattern interrupts in AGENTS carry the most operational weight.
LLM The reasoning engine. Stateless, interchangeable. gpt-4.1-mini for the main loop (upgraded from gpt-4o-mini after catching it interpreting strict-output prompts instead of forwarding them). Model routing for the writer sub-agent when the task demands literal output.
Filesystem Persistent state. The only place things actually change. ~/.openclaw/workspace/ for working state, ~/.openclaw/memory/ for learned facts, jobs.json for scheduled work, runs/<jobId>.jsonl for cron run records. Filesystem is the only ground truth; the chat transcript is not.
Cron The autonomy primitive. Runs the loop when no human is watching. OpenClaw's internal scheduler reads jobs.json and spawns isolated sessions directly into the same agent loop. Not OS crontab. Daily reflection at 0 4 * * * Europe/Berlin is the concrete example.

For a teardown of how each primitive scales beyond one user and one machine, see docs/at-scale.md.


The five problems of autonomous systems

In dependency order. A system that skips a level fails at the next one. This is the load-bearing section for how I think about what I built.

  1. Isolation. If one run can corrupt another, nothing else matters. NAVis solves this with session types: main, isolated (for cron), and named (persistent across runs). Each session gets its own context window and its own payload shape (systemEvent vs agentTurn). Mixing them fails silently, which is how I found out they were distinct.
  2. State and behaviour. A stateless LLM cannot be an agent. NAVis keeps all state on the filesystem in markdown, loads it per turn, and lets the model write back. The four identity files are how behaviour survives the end of a session.
  3. Intelligence. The reasoning step. Solved by calling the LLM with the assembled context. Interesting only once isolation and state are sound, because without them the model has nothing stable to reason over.
  4. Action surface. What the agent can touch outside its own process. NAVis exposes this through shell scripts and a small set of skills. Skills are instructions the model chooses to follow; scripts run regardless. For anything that must execute exactly, I use a script.
  5. Scheduling. Autonomy. Only works when 1 through 4 are solid, because a scheduled run is an unattended run. NAVis uses the OpenClaw internal scheduler, not OS cron. Each scheduled job is an isolated session that reruns the full loop against the same filesystem.

PRACtis (the project I'm building next) is another instance of this same five-problem stack, applied to an intelligence pipeline rather than a personal assistant. Different surface, same primitives.


Four security questions

Four questions that frame every inbound and outbound decision in NAVis, grounded in the actual setup.

Who can reach my Claw? (inbound) The gateway binds only to loopback (127.0.0.1:18789 for the webchat and API, 127.0.0.1:18791 for the internal RPC the CLI uses). Nothing listens on eth0 or tailscale0. The only way in is to already be on the same host, or to be SSH'd in and port-forward loopback over Tailscale. I verified this with live ss -tlnp against the gateway process. Hetzner's firewall on eth0 is a second layer, but the bind choice is what enforces the isolation.

If it reaches, can it act? (auth) Yes: the gateway requires a token on every request, independent of how you arrived on loopback. Two layers: you must be able to reach the port, and you must present the token. Reaching the port is not sufficient.

What can my Claw reach out to? (outbound) The LLM API is always open. Brave Search API for web search. Gmail IMAP and SMTP for the morning summary and outbound email. Telegram Bot API for the chat interface. Every other outbound path is closed. Outbound is never zero for an agent; the question is whether each outbound channel is justified and bounded.

Is it acting on its own? (autonomy) Yes, but on a leash. One cron job today: daily reflection at 04:00 Europe/Berlin. The heartbeat mechanism (continuous self-invocation) is deliberately disabled. Outbound email runs only after a confirmation turn from me. The principle: contain before extend. Each new autonomous capability gets added only after the previous ones have been observed to behave.


My mental model of the internals

Four distinctions that took time to work out and that I would not have internalised without building the system end to end. These are not errors in any documentation; they are the things I had to ground in my own setup before they clicked. Full teardown in MENTAL-MODEL.md.

  • OS cron and OpenClaw's internal scheduler are distinct mechanisms. Jobs created inside OpenClaw persist to jobs.json and run through the gateway, not through /etc/crontab. Both exist on the VPS; only one runs NAVis's scheduled work.
  • The cron chain for internal jobs does not involve a shell-to-port handshake. The gateway's scheduler sees a job due, reads jobs.json, creates an isolated session, loads the identity files, calls the LLM, posts the result back to the main session, closes the session, and writes a run record. No port knock from outside.
  • jobs.json is the persistence layer that makes scheduled behaviour survive gateway restarts. Diagrams that show scheduling without this layer leave out where the state actually lives. If you want to know whether a scheduled job will run after a restart, this file is the answer.
  • There are three session kinds, not two. Main sessions carry systemEvent payloads, isolated sessions carry agentTurn payloads, and named sessions (session:<name>) persist across runs as a third category. Payload type and session type are linked and must match. Mixing them is the failure mode that taught me they were distinct.

What broke before it worked

Full log in docs/what-broke.md. The summary.

Agent self-reporting is not ground truth. The model confirmed writes that never happened -- twice, in two distinct forms. First: prose describing an action with no tool call emitted. Second: tool call executed but content paraphrased, not verbatim. Both look identical in the chat transcript. The filesystem told a different story. Resolution: verify every write with cat, ls, or git log. Agent-reported success is a hypothesis, not a fact.

The wrong mental model sends you to the wrong place. The cron chain most people assume -- OS cron fires a shell command at the gateway port -- is wrong. The gateway's own internal scheduler reads jobs.json and creates isolated sessions directly inside the same process. This produced two silent near-failures: a cron job with a correct sessionTarget but wrong sessionKey, and a writer agent directory that existed while the load-bearing config key in openclaw.json did not. Caught only by reading the right file at the right level of detail.

Autonomous triggers need a permission model before anything else. A third-party skill runs with the agent's full credentials from the moment it is installed -- before you have verified what it can access. The imap-smtp-email community skill had four undocumented deployment gaps and inherited full agent permissions on install. Capability before constraints is the most common mistake in AI product design.

Deployment context determines capability surface. Skills installed in the workspace tier are invisible to isolated cron sessions. A skill that works in interactive webchat does not exist in the scheduled execution context. This is a product spec question, not an engineering detail -- it must be answered before shipping any autonomous feature.

The architectural fix matters more than the model upgrade. Upgrading from gpt-4o-mini to gpt-4.1-mini narrowed the interpretation gap but did not close it. For any task requiring deterministic output, the right fix is a shell script, not a better model. Scripts execute. Models interpret. They are not the same thing.


What I would design differently next time

In DECISIONS.md. Short version: I would build the script-as-tool layer before the skill layer, not after. I would treat jobs.json as a first-class piece of infrastructure from the start rather than an implementation detail. I would model the three session types before writing a single cron job. And I would define the permission model before enabling any autonomous trigger.

The design arc underneath all of it: contain it → define it → stay reachable → test its initiative → control its scope → protect it from external manipulation → gate its irreversible actions → limit each agent's belief about itself → verify it can report its own state. That sequence is not arbitrary -- each step is a precondition for the next.


Stack

Hetzner CPX22 (Ubuntu, Nuremberg) · Tailscale (WireGuard mesh) · systemd user services · OpenClaw gateway (Node.js, loopback-only) · gpt-4.1-mini · Telegram Bot API (long-poll) · Brave Search API · Gmail IMAP/SMTP · markdown filesystem for state.

License

MIT. See LICENSE.

About

A personal AI agent built from the primitives up. Security, identity, scheduling, and trust, all on a Hetzner VPS.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages