Aromba — Skills
The base install is the lightest thing that is still useful. Everything else is a skill: installed on purpose, weighed before install, removable in one tap. This is how Aromba stays under its weight budget (04-assistant.md section 3) on every server while doing a lot on the servers that need it.
1. Base (always installed)
Engine, bot, assistant with its core tools, and the system skill. Nothing else. No voice, no web access, no stack-specific chores.
system contains what every server needs regardless of stack, OS level only: watchers for disk, memory and swap, load, failed logins, service health, pending updates and reboot-required; apt cache, journal and temp cleanup (green); old logs beyond rotation (yellow); security updates, fail2ban and restarting failed services (red); time sync (green). Language caches belong to node, python and rust. The morning receipt, the weekly inventory and the calendar (Aromba’s plan and the user’s own items, PLAN D107) are built into the daemon, not chore files. Its chores live in chores/core/ (02-chores.md section 7); it is the one skill that cannot be disabled or removed.
2. Four kinds of skill
| Kind | Contains | Weight | Example |
|---|---|---|---|
| Chores | chores and watchers for one stack | KB | docker, postgres |
| Capability | a binary or model the daemon calls | MB to hundreds of MB | voice (whisper), docs (web fetch) |
| Integration | assistant tools and chores for an external service; needs the user’s token | KB | github, cloudflare |
| Knowledge | only text added to the assistant’s context; runs nothing | KB, zero RAM | nextjs runbook |
Instruments are not skills (15-instruments.md, PLAN D210). An instrument is a page built for a product on this machine, kept as data in the Worker’s own library and shown by the product; it brings nothing that runs here, so it needs no manifest, no cage and no install. What could become a skill later is a library of instruments one product has proved, signed and shipped to other servers that serve the same kind of product.
A bundle is a skill that installs other skills: one tap for a whole stack. It holds no files of its own — no chores, no text, no tools, nothing to pack and nothing to hash — so it is never installed: it is a list of names the chat resolves. /skills <n> on one says what would be installed, what is already there, and what is missing from the catalog; the tap installs each member exactly as installing it alone would.
A member that asks for the PIN is not installed by the tap. A PIN’s ticket is good once, and asking for it four times to install four skills teaches somebody to type it without reading; those members are named, and offered on their own from /skills.
A skill may also bring tools: tools/<name>.sh and tools/<name>.json beside it, the script and the JSON Schema of what the model may pass it. A tool is the smallest privileged path in Aromba and deliberately so — it runs in a cage with nothing writable at all, not even the skill’s own paths, so it reads what is not fenced, answers on standard output and changes nothing. That one rule is what makes it safe to run on a model’s word rather than on a button; a tool that needs to write is a chore, which has an approval, a snapshot and a receipt. What the model passes arrives as environment variables (T_<NAME>), never as arguments: there is no shell line to break out of. Thirty seconds, eight arguments, 16 KB of output, and the answer is redacted like every other. A tool with no schema beside it does not install (PLAN D143).
A capability skill brings a program and the data it reads — the two things too large to ship in the daemon and too rarely wanted to ship to everybody. They are not in the tarball: the manifest names an address for each and the SHA-256 of what is at it, and the tarball is what gets signed, so the digest is what makes the bytes trusted and not the address. The Worker fetches (the Worker is the half with a network), writes to a folder of its own, and hands each file to the Warden; the Warden checks it against the digest its record of the manifest carries, and a file that does not match is not kept — the Worker’s copy is dropped either way. Both files live under /var/lib/aromba/models/<skill>/, the Warden’s, and nothing the skill brought is writable by anything afterwards. Running the program is the Warden’s too, in a cage with no network at all and nothing writable but one folder holding its input, emptied after: a program that parses what a stranger sent is exactly the kind of program to put inside one. A capability skill asks for the network, because a download is one, so it waits for the PIN.
A knowledge skill’s tarball is skill.toml and knowledge/*.md, one level down, and nothing else: no chores, and no permissions at all — a runbook runs nothing, so a permission on one is a permission nobody will ever use and somebody may one day. The Warden keeps the hash of every file it brought, as it does for a chore. The assistant is told the names in its card and reads one with the runbook tool when a question is about that stack.
3. Manifest
# skills/postgres/skill.toml
name = "postgres"
kind = "chores"
version = "1.0.0"
description = "Backups, restore checks and health watchers for PostgreSQL"
min_aromba = "0.1.38" # the oldest release this skill has run on
needs = ["pg_dump", "psql"]
[weight]
disk_mb = 0.2
ram_mb = 0
[permissions]
paths_read = ["/var/lib/postgresql/*/main/postgresql.conf", "/etc/postgresql"]
paths_write = ["/var/lib/aromba-backups/postgres"]
root_commands = ["/usr/bin/pg_dump"]
network = false
hosts = []
# The read-only questions its chores may put to the Warden before a run,
# each as the header writes it: Docker, systemd and apt live behind
# sockets the cage hides (PLAN D111). The Warden still answers only what
# its own read-only list allows.
asks = ["systemctl is-active postgresql"]
[suggest]
when = ["binary:postgres", "port:5432", "container:postgres"]
A capability skill adds one more block, and it is the whole of what makes a fetched file trusted:
[capability]
binary = "https://aro.mba/bin/whisper-cli-linux-{arch}" # {arch} is amd64 or arm64
binary_amd64 = "f337459a…" # SHA-256, one per architecture
binary_arm64 = "c5c842df…"
model = "https://aro.mba/models/ggml-base.bin"
model_sha256 = "60ed5bc3…"
model_mb = 141 # what the chat says before the wait
min_aromba is the oldest release the skill has been run on, and the Warden refuses an install on anything older, by name — a key nobody reads is a promise nobody keeps (PLAN D146). A Warden whose own version does not parse, which is every dev build, checks nothing.
Permissions are a ceiling: no chore or tool inside the skill can get more than the manifest declares, and nothing can widen them after install. A chore whose header asks a question the manifest does not declare is refused at install, and at the run. The Warden applies the manifest as the cage for every chore in the skill. The ceiling the Warden applies is its own record of the manifest as it was at install, with the hash of every chore the skill brought: a file changed under /opt/aromba/skills afterwards does not run. An unknown key in a manifest is refused — a permission nobody reads must not look granted. network = true lets the skill’s chores reach the network — any host, until the cage enforces hosts; without it they run in a network namespace of their own, with no route out. A skill whose chores or tools fetch, pull or upload (git, docker’s image pulls, backup-s3, deploy, docs) asks for it, and so waits for the PIN. root_commands is the other PIN permission: the binaries its red chores may ask the Warden to run outside the cage, each written as an absolute path, each shown at install. A skill may name only a binary the chore-action table already runs — systemctl, timedatectl, apt-get, docker (05 §5) — so what a skill gains is the right to ask for a request the Warden already judges, with the same argument grammar a core chore’s request meets, and never a new binary to run as root. A request from a chore that is not red, or for a binary its own manifest did not declare, is refused (PLAN D134). Without the network a chore also has no host loopback and no DNS (Ubuntu’s resolver is at 127.0.0.53): a step that needs either asks for the network, or is a root command run outside the cage.
4. Install flow
Three doors, same flow: /skills in the bot, the Skills page on the dashboard, aromba skill add <name> in the terminal. The assistant can only propose (skill_propose). The catalog itself is served from https://aro.mba/skills/: index.json lists every skill with its description, weight, permissions, chores and the digest of its tarball, and both the index and each tarball are signed with the release key — the Warden checks the signature itself, so the address they came from is not what makes them trusted (PLAN D127).
- The catalog entry shows: description, kind, weight, every permission in plain words, the read-only questions the Warden will put on the skill’s behalf (
asks, word for word: what the cage hides — Docker’s socket, systemd’s, the firewall — reaches a chore this way and no other), chores inside with their colors, what the first scan found that suggests it. - Tap install → if the skill asks for root commands, the network or network hosts, PIN. Otherwise one tap.
- Chores inside start at their catalog color; the probation week still applies to yellow and red.
- Disable: one tap, chores stop, files stay. Remove: second tap, files gone,
mine/untouched.
An integration skill also asks what it connects to — in a root terminal when that is a secret, as a calendar’s address is (PLAN D101) — and which of its behaviors to turn on.
The first scan suggests at most three skills at once, by evidence: “Found Docker and Postgres. Install their skills?” [Both] [Choose] [Not now]. Weight and permissions are visible before the tap. Bundles are suggested when a whole stack is detected.
5. First official skills
Phase 2 — with chores, suggested by the scan
| Skill | Suggested when | Inside |
|---|---|---|
git | repos found | the only thing on a server that cannot be rebuilt from somewhere else. A disk that dies takes the containers, the packages and the configuration, and all of those come back; what does not is the change somebody made on the server at two in the morning and never committed, or committed and never pushed. One chore, daily, over the repositories under the folders you name, two levels deep: how many files are uncommitted, how many commits are ahead of their upstream, and which repositories have no upstream at all. A repository that is both dirty and unpushed is one repository at risk, not two. It reads and fetches nothing; fetch --prune and the upstream-changes report would reach the network and come with the PIN, later |
http | curl present | “is it answering”: the addresses named with aromba url add are fetched every five minutes, with the status and how long it took; two failures in a row is the alert, and a slow answer is its own. A tcp://host:port address is watched by the dependency watcher instead — the port is opened and closed, no password and no query, because a service that is up while its database is not is the alert nobody gets today. The certificate watcher reads the dates of the https:// ones once a day and speaks two weeks before one runs out — the failure that takes a site with it and says nothing first. It reaches the network, so it comes with the PIN |
docker | Docker present | the log-error watcher — how many errors in a container’s last 200 lines against the same watcher’s last count, with the newest quoted — and the container watcher — what is restarting, what is down, and what the kernel killed for memory, from questions the Warden puts for it (PLAN D111) — then prune (green), orphan volumes (red), log rotation for containers; pre-pulling compose images asks for the network, so it comes with the PIN |
postgres | Postgres binary, port or container | the connection watch, built (PLAN D233): every connection is a process titled with its user, its database and its state, and the host sees them whether Postgres runs on it or in a container, so the watcher reads the process list — no password, no SQL, no query text — and says when connections pass four fifths of the limit, and when a client has sat idle inside a transaction for two runs in a row, holding its locks. Still to come: the nightly dump kept locally, the weekly restore test into a temp DB, the size watcher and the “no backup” nudge, on a root command of their own |
mysql | MySQL/MariaDB | same shape as postgres |
node | package.json / node_modules | npm/pnpm/yarn cache cleanup, dead node_modules (yellow), pm2 watcher, “port taken by X since May” |
python | venvs / pip | pip cache, dead venvs (yellow), gunicorn/uvicorn watcher |
rust | cargo / target found | cargo registry cache cleanup, target/ of dead projects (yellow) |
nginx | nginx present | two things nobody watches until the day they matter. What the edge is answering with: the share of the last few thousand requests that came back 5xx, hourly, against its own last reading — a site answering 500 to a fifth of its requests is down for a fifth of the people using it, and nothing else on the server notices, because the process is running, the port is open and the disk is fine. Whether the configuration still parses: nginx -t once a day, which is on the Warden’s read-only list — a broken file on disk is a site that will not come back after the next reboot, and the day to hear about it is not the day the machine restarts. The certificate dates are the http skill’s and are not repeated here; access-log rotation is logrotate’s, which the setup checklist looks for |
caddy | Caddy present | certificate and 5xx watchers, config check |
Phase 3 — with the assistant
| Skill | Kind | Inside |
|---|---|---|
voice | capability, +143 MB disk, +300 MB RAM while transcribing | send a voice message and it is answered like anything typed. whisper.cpp, built by us from a pinned tag with no tuning to the machine that built it, and the base model, upstream’s own bytes; each checked against the digest in the signed manifest before it is kept. Nothing said into this server leaves it: there is no account anywhere, no key and no request — the relay carries the bytes from Telegram and keeps none, the Worker decodes the Opus in Go rather than handing a stranger’s bytes to C++, and the words are made on the machine, in a cage with no network. A sentence takes a few seconds of one core — three on a laptop, seven on a small VPS — at low priority. It is offered once, on the first voice message somebody sends, which is the moment it means anything; what was heard is quoted back before it is acted on, so a word heard wrong is a word you can see was heard wrong. Never in the base |
docs | one tool | read_docs: the assistant reads a page of the documentation of what this server runs. The allowlist — Docker, nginx, Postgres, Node, Next.js, Ubuntu, Debian — is in the tool’s own script, which is signed with the release key like every other file of an official skill, because the cage cannot hold a skill to named hosts yet (§3): the manifest asks for the network, the script is what keeps “the network” to ten addresses. What comes back is the page’s text, tags and scripts stripped, capped at 12 KB. The skill asks for the network, so it comes with the PIN |
backup-s3 | chores | the answer to what the first scan says about nearly every server, “No backups found”. One nightly chore: the folders you name, copied with rclone to a remote you made with rclone config at your own terminal — the skill carries no credentials and never asks for any; what Aromba knows is the remote’s name. It keeps its own memory, so one bad night is a receipt line and a run of them is the alert, with how long it has been since a whole one worked: a backup nobody checks is the one that has been broken since March. It writes nothing on this server, and uploads, so it waits for the PIN |
deploy | chores, red | “ship main”: the folder, the branch and the unit are yours to name. It stops on an unclean tree (a deploy that throws away an uncommitted fix is worse than no deploy) and on anything that is not a fast-forward (a deploy is not a merge somebody has to read later), installs only when the lockfile moved, builds when the project says it has a build, and asks the Warden to restart the unit — systemctl restart, the action table’s own, so the unit name meets the same grammar a core chore’s request does. A second chore runs after: it and says what systemd is unhappy about, naming the unit that was shipped first. Every run red, which is how it gets its snapshot: the Warden copies the folder before the run and undo puts it back for seven days; a project too large to snapshot is a run that cannot be undone, which asks for the PIN — the same rule, not a new one. The folder must be under /srv or /var/www: a deploy that may write a home may write ~/.ssh, and a skill’s ceiling is the same for everybody who installs it |
claude-code | chores | for the person whose server runs code they wrote with Claude Code. One chore writes server.md beside the code once a day — what this machine is, what runs on it, what listens where — so the answer is already in the folder when Claude Code opens it; it writes that one file, and a server.md somebody wrote themselves is left alone and said so. The other reads the repository’s own log and says what Claude Code changed since yesterday: its commits carry a Co-Authored-By line, so the count, the share of the day’s commits and the folders it worked in come from the log and from nothing else. It talks to no API and holds no key. The night task through claude -p waits for a skill that may run a binary of its own (PLAN D134 admits only the requests the Warden already answers) |
docker-clean | chores, red, PIN | what Docker leaves behind on a server that builds (PLAN D230). One chore, at night, off until turned on: it reads the dangling images and Docker’s own disk count through the Warden, and past a gigabyte of either — the thresholds are its params — asks the Warden for docker image prune -f or docker builder prune -f, which it runs outside the cage. Dangling images and the build cache not in use only: never -a, a volume or a container, since those can be data or an image a stopped container needs to start. Its own skill, so the docker watchers do not widen where they are installed; root_commands is why its install asks for the PIN |
leaks | chores | a process that keeps growing until the kernel kills it, said while there is still a day to act (PLAN D232). One watcher, hourly, over the eight largest processes as the Warden reads them and the kernel’s own available memory: each is compared with itself an hour ago, by pid, through the watcher’s own last result, and one that has grown at every run for six hours, at a rate that ends the available memory within a day, is said with its rate and the hours left. It writes nothing |
cron | chores | the jobs that run on a timer and fail without a word (PLAN D227). One watcher, hourly, over cron’s log in the journal, which the Warden reads for it: the jobs that failed in the last hour, by exit status — cron writes “grandchild failed with exit status” and nobody reads it — and, for the job you name by a piece of its command line, when it last started against how often it should (every, in hours, up to the day the journal answer covers). The output a job printed on a server with no mail is noted in the receipt. It writes nothing and reaches nothing |
security-audit | chores | the “is it set up right” checklist, daily: is a firewall on (ufw or an nftables ruleset that drops), does ssh take passwords, can root log in with one, do security updates install themselves, is there swap, is anything rotating the logs, and what is listening on every address that is not ssh or a web port. A score, and the line that fixes each one — to run, not to tap: a caretaker that turns a firewall on at three in the morning is how a server is lost, and changing sshd from a chat is the same mistake. No check here is a button, and the two reasons agree: a firewall or an sshd changed from a chat can take the server away from its owner (05 §7), and only a core chore may ask the Warden to run anything outside the cage — which is what the other fixes, a package install or a swapon, would be (PLAN D133) |
calendar | built into the Worker | brings the user’s own calendars into Aromba’s (PLAN D107, D101). It needed no skill in the end: reading a private iCal address is the Worker’s own job and always was, so aromba calendar add <name> at a root terminal asks for the address without showing it, writes it into calendars.toml (mode 600, hidden from every cage), and the daemon reads every feed each quarter of an hour. What it reads is replaced whole, so an event moved or deleted in the calendar follows within the quarter hour; what Aromba knows about an event — that its reminder went out, that its demo check has run — is kept across a read. An imported event is shown in /today with the calendar it came from and no Delete: it is not ours to delete. The rest as before — read through their private iCal addresses by the Worker’s own reader (Google Calendar, or any calendar that has one) and shown in /today beside their own items; demo check, quiet during marked events and asks in free time for their events, the assistant’s calendar tool (09-features.md §13). Never writes to a calendar (PLAN D101) |
nextjs | knowledge | a runbook and nothing else: what actually goes wrong with Next.js on a VPS, in the order it goes wrong — the port already taken, the build the kernel killed for memory, .env.local that production never reads, NEXT_PUBLIC_ baked in at build time, .next/cache thrown away by a deploy — and the five things to look at, in order. It runs nothing and asks for nothing. The assistant reads it with the runbook tool when a question is about this stack; the card names the runbooks and carries none of their text, because a page in the card is a page paid for in every turn |
design-app, design-website, design-document | knowledge | how a page of each occasion should look: its rules, how to choose a direction and write its tokens before the markup, the defaults a model falls into, a handful of popular styles to start from, and the review the designer’s second pass is held to (PLAN D238). Read by the build turn for a product’s page of that occasion (15-instruments.md, occasion); the chat can read one like any runbook |
stack-nextjs | bundle | git + node + nginx + http + nextjs: the repositories, the caches, the edge, the up-check and the runbook. postgres joins it when that skill exists |
stack-bot | bundle | git + docker + data: what is restarting, what its log says, whether the data is still moving, and the work that is nowhere else |
Later
github (PRs, failed actions), cloudflare (DNS, certificates), vercel, uptime (external URL checks), redis, mongo, supabase, status-page, postmortem, telegram-bots, go (build cache), mac (Old Downloads and the Mac-only sweeps from 02-chores.md, phase 6), and community skills after review.
6. Rules
- A skill never checks the tier. Skills are free in every tier.
- Official skills are signed with the release key and follow catalog colors. Community skills run red until reviewed and promoted.
- Knowledge skills run nothing and are loaded into the assistant’s context only while enabled.
- Capability skills declare their real weight; the Skills page shows the total against the user’s skills budget (default 1 GB, adjustable). What they fetch is trusted by its digest in the signed manifest and never by the address it came from, and what they run runs in a cage with no network.
- A skill can’t add root commands, the network or network hosts silently; each is a PIN action at install — or, for a host the user names after install (a calendar’s address), confirmed at the root terminal that names it.
- Updates to a skill that change its permissions are shown and confirmed like a fresh install. What is installed is what was shown: the offer carries the hash of the package, and the install names it.
- A skill’s chores start off, and never take the name of a chore the base ships.
- The base never grows. If something could be a skill, it is one. One exception so far, decided in PLAN: the calendar — Aromba’s own, and the reader that merges the user’s into it — which no skill can carry because the Worker runs no skill’s code (D101, D107).