↓ Skip to main content

Ambrogio 0.3: How It Works Today Across Conversations, Three Agent Types, Projects, and the Landlock Sandbox

Roberto Tazzoli
Author
Roberto Tazzoli
Passionate about declarative infrastructure, self-hosting, and Kubernetes.

From Experiments to the Version We Use Every Day
#

In the previous two articles I covered how Ambrogio 0.1 was born (a single Go binary with SQLite keeping agents alive) and how in 0.2 we tested task trees on Git worktrees with same-session review.

Version 0.2 had shown two clear limits: task trees generated by agents multiplied out of control, and two different engines had accumulated inside the same binary (the old 0.1/0.2 code and the new one), to the point where the web page read stale in-memory counters and displayed zero active workers even while agents were actively working.

With Ambrogio 0.3 — after deleting 377 files and over 67,700 lines of old code across two cleanup queues — we reached the clean, stable version we use in the lab today to track all our work, which is also presented on the project’s dedicated site: ambrogio.tazlab.net.

1. From Trees to Queues: A Single Session Carried Across All Steps
#

The main shift in 0.3 is moving from task trees to linear queues.

Today the unit of work is a queue: an ordered sequence of steps with a maximum step ceiling declared when I accept it (steps_cap). No work starts on its own anymore: a request can be opened at any time, but it stays parked until I approve it and decide how many steps it may take. If the queue hits its step limit before finishing, it stops and returns to me explaining what it completed and what is left, without getting lost.

The core feature is that the entire queue shares a single agent session saved on disk (session_id):

  • the third step in the queue already knows everything that was read and decided in the first step, without having to re-read the repository from scratch and without wasting tokens;
  • because the session lives on disk, it also survives a pause or a restart of the Ambrogio binary.

Even talking to Ambrogio uses the exact same mechanism: a conversation is not a separate table, but a queue of kind conversation. Every message I send adds a step to the queue, and the agent’s reply becomes its summary (brief). Conversations have dedicated slots (conversation_slots) so they never wait behind heavy background jobs; after a period of inactivity they pause automatically, and as soon as I send a new message they resume the exact same session from disk.

2. Three Agent Types and the Skill That Opens the Door to Any External Tool
#

To avoid giving every agent the same permissions and cost, Ambrogio 0.3 distinguishes three types of agents, each with a specific role:

Agent TypeWho Starts ItWhat It Can DoWhat It Is For
1. Internal agent (worker-internal)Started by AmbrogioReads code (read, glob, grep), opens and organizes tasks, reads and writes project documents in the database. Has no bash and cannot modify files on disk.Managing internal structure, planning work, and keeping documents and projects updated at minimal cost.
2. Piloted external agent (worker-external)Started by Ambrogio over ACP inside a Landlock sandboxHas full operational tools (read, write, edit, bash, browser) confined strictly to the queue’s worktree, with no Git credentials or gopass access.Executing code work in complete isolation, leaving push, Pull Request, and merge operations to the Go engine.
3. Free external agent (free external agent)Any agent or harness started outside AmbrogioBooks open tasks via API (GET /api/v1/free, POST /api/v1/book) and delivers the result (POST /api/v1/deliver).Allowing any external agent or environment to work alongside Ambrogio while keeping all traceability in one place.

The third type — the free external agent — is what made the system truly open.

Instead of forcing me to use only internal workers spawned by the binary, Ambrogio exposes three API endpoints designed for external workers:

  1. GET /api/v1/free to list open tasks that nobody has claimed yet;
  2. POST /api/v1/book to book a task with an automatic expiration (if the external agent hangs or disappears, the booking expires on its own without needing a watchdog);
  3. POST /api/v1/deliver to deliver the completed work and propose any follow-up steps.

The most convenient part is how an external agent learns to use all of this: Ambrogio directly provides its own skill (SKILL.md) explaining the engine’s structure, the HTTP doors, the booking lifecycle, and the delivery format. Any external agent or harness — even one that has never seen Ambrogio before — can load that skill at the start of its session and immediately begin claiming tasks, updating them, and delivering work inside Ambrogio.

3. Projects, Documents, Cron Jobs, and the Native Web UI (/ui)
#

Everything that happens in the engine is recorded in the local SQLite file and can be inspected in real time through the native web interface (/ui), embedded directly inside the Go binary and tuned to work comfortably even on a phone screen (320×568).

From the web page I manage four elements that keep the lab organized:

  • Projects and documents: every job belongs to a project, and every project stores its own documents (plan, roadmap, progress) in the database. Documents are not optional: the task chain tells what was done, while documents explain why. That is why the engine’s deterministic review checks that the project’s minimum required documents (MinDocs) have been written and refuses to close the queue if they are missing.
  • Cron jobs: I can declare periodic automated checks (for example, a job that periodically verifies whether the repository compiles and updates the BUILD.md document). If the check finds an error, it does not silently touch code on its own: it records the actual error and opens a task proposal that waits for my confirmation.
  • Full traceability and contestation: every event (task_event), every single execution with its duration and outcome (run), every delivery (report), and every verification (review) stays saved in the database. And if I later notice that a task closed as completed does not actually work, there is an explicit command (POST /api/v1/contest) that opens a child task linked to the previous closure to fix the defect without erasing history.

4. The Engine as Git “Butler”, the Two Doors, and the Landlock Sandbox
#

For workers piloted by Ambrogio (worker-external), security and Git management are not left to the AI model’s goodwill, but enforced by Go code and the Linux kernel:

  1. The engine owns Git, not the agent: every queue works in its own local worktree and branch. The worker writes code, runs tests, and runs git commit locally, but holds no GitHub token and cannot run git push. When the queue is ready, the Go engine’s landing train validates the branch, pushes to GitHub, opens the Pull Request, waits for checks, and merges sequentially (attempting a mechanical rebase first if needed). And the very first live test of this mechanism was an automated update to the ambrogio.tazlab.net site itself, merged by queue #67 of the engine.
  2. The Landlock sandbox for every worker: before executing a worker, the ambrogio-sandbox launcher applies a Linux kernel Landlock policy (NoNewPrivs=1) to the process. The worker can write exclusively inside that run’s worktree; it cannot write anywhere else on the filesystem and cannot read ~/.gnupg or invoke gopass (Permission denied).
  3. Two separate HTTP doors (7806 and 7807): port 7806 is reserved for me and exposes all administration commands (accepting queues, changing settings, opening conversations); port 7807 (AMBROGIO_DOOR) is what agents see and exposes only the few endpoints needed to read their assignment and deliver the result. Any attempt by an agent to call an operator command receives 403 Forbidden.
  4. The internal Vault and passphrase: the engine’s secrets live encrypted inside SQLite (AES-256-GCM with a key derived via Argon2id from the passphrase I enter at startup in the web page bar). Changing the passphrase also strictly requires entering both the old and new passphrases ({old, new}), re-encrypting the vault in a single transaction and immediately revoking all open sessions, so nobody can change the key if they find a device with an unlocked session.

Conclusions
#

The journey from 0.1 to 0.3 brought me to the exact place I was looking for when I turned off the external orchestrators:

  • a single Go binary that owns a single directory (/opt/ambrogio/data) and keeps the entire history in one SQLite file;
  • linear queues that persist their session on disk, so the agent that starts the work continues it without burning tokens re-reading context at every step;
  • projects, documents, cron jobs, and conversations manageable from a single web page even on a phone;
  • and three tiers of agents — internal agents for organization, piloted external agents in a Landlock sandbox for automated worktrees, and free external agents guided by Ambrogio’s skill — making it possible to plug any new tool or external agent into the engine without losing control or task traceability.

All details on the project’s architecture and operation are also available at ambrogio.tazlab.net.


Comments