Aromba — Instruments and the intake
Aromba can be the hands of another product on the same machine (PLAN
D207, D210). The product has the brain and the people: it knows what one
person wants and holds that person’s data. Aromba has the builder: it turns
what is wanted into an instrument — a page the product fills with that
person’s data and shows to them — and keeps every instrument it made, so
the next person who wants the same thing gets it in seconds for a fraction
of a cent.
This page is the contract. A product that implements §1–§5 gets hands, and nothing in Aromba changes for it. whoopy is the first.
0. What holds, whatever a product does
- Aromba never sees a person’s data. It builds from the product’s own words for what is wanted, a schema of the settings one person may choose, and a made-up example of the data. The product fills the instrument on its own side. A token or a person’s data in anything the product sends is never read: Aromba decodes the fields below and no others, and passes the words and the example through its redactor before any prompt anyway.
- Nothing a model writes runs on this server. An instrument is data, kept in the Worker’s own folder. The product shows it, under its own no-network content security policy.
- A person approves every new instrument before it reaches anyone: the
owner, in the chat, looking at the page itself. What the library already
has goes back with no tap, because it was approved once and the settings
fit its schema. One exception, the owner’s own rule (PLAN D277): at a
root terminal,
aromba intake auto <product> <occasion>sends that product’s pages for that occasion — a page one of its own users ordered — as soon as they pass the checks, with no tap. Such a page is always a page of its own and replaces nothing, answers no other job until the owner adds it to the library, and still comes to the chat as the page itself, with [Add to the library] [Leave it]. The product cannot set the rule;aromba intake auto <product> -takes it back. - The product is on this machine, or on its own server over https.
Aromba asks it for work over loopback — or, when the product keeps what
must not share a machine with anybody’s root, such as its users’ trading
keys, on the product’s own server over https, from a small server of
Aromba’s own (PLAN D242) — and never anywhere else. Nothing reaches into
Aromba. The owner names the product at a root terminal, and its host goes
into
network.tomllike every other host this server talks to (05 §10); plain http is refused anywhere but loopback, so the key never crosses a network in the clear.
1. Naming a product
aromba intake add whoopy http://127.0.0.1:3000/factory -header x-factory-key
The address must be http:// on 127.0.0.1, ::1 or localhost, with a
port. Anything else is refused. The key is asked for at the terminal
without being shown, and is kept in /etc/aromba/worker/intake.toml, mode
0600, the Worker’s own file. It is never an argument, never in a journal
line, never in the chat and never in a prompt. -header names the header
the key goes in; without it the key goes as Authorization: Bearer <key>.
Naming a product asks its brief at once, so the terminal can say whether
the address and the key are right. aromba intake lists the named
products, and aromba intake remove <name> stops asking one.
While a product is named, the Worker asks it for work every 20 seconds. That is one request to loopback, and nothing is asked while no product is named.
2. The six endpoints
Each one sits under the address and carries the key. Every answer is JSON, 256 KB at most.
| Endpoint | Aromba sends | The product answers |
|---|---|---|
GET brief | — | {"brief": "…", "placeholders": ["params", "state"]} |
POST claim | {} | a job, or {"job_id": null} when there is none |
POST publish | an instrument for one job (§4) | anything 2xx |
POST failed | {"job_id", "error"}, and "detail" for a job under the owner’s rule | anything 2xx |
POST spent | {"job_id", "usd"} | {"over": false, "spent": 0.0021} |
POST incident | the answer to an incident’s report (§ The firefighter) | anything 2xx |
The brief is what an instrument for this product must be: how it fills a page, what the data under each placeholder looks like, and anything else the builder must know. It is the start of every build prompt, so it is short, stable, and written for a model. It is fetched for every job, so an edit reaches the next page written (PLAN D237).
A job:
{"job_id": "J-17", "asked": "a nutrition board in my own colours", "need": "page",
"input_schema": {"type": "object", "properties": {"palette": {"type": "array"}}},
"example": {"state": {"meals": [{"name": "oats", "kcal": 420}]}},
"budget_usd": 2}
askedis the product’s own words for what one person wants: normalised, never the person’s message, and never their name, contact or data.needis the kind;pageis the one there is today, and a job for any other kind is failed at once.input_schemais optional: settings the product suggests.exampleis made-up data by placeholder, in the shape the product’s real data has. It is what the builder writes against, and what the owner’s preview is filled with.budget_usdis the most the job may cost. A build that would pass it stops.directionis optional: the kind of work —health,hobby— and it picks the model the page is written on. Each direction has three inproviders.toml, the best, the default and the cheapest, chosen by writing the same pages on each; the owner moves a direction between them with/model. A direction with no models of its own is written by the provider’swriter(PLAN D224).done_whenis optional: how the product will know the page is right, in its own words (“both windows print their n; under 10 days it says too few days”). The writer is held to it. Scrubbed likeasked, and a thousand letters at most.occasionis optional: what the page is —app(a screen inside the product; what every page was before, and what none means),website,document(read, sent and printed on A4). The page is written to that occasion’s design skill,design-<occasion>, when it is installed — its rules, its styles and the review of the designer’s second pass — and to the built-in brief when it is not, which the journal says for any occasion butapp(PLAN D238). A word that names no design, such asuser_request, is written asapp; the owner’s rule of no tap is read against the word as the product wrote it (PLAN D277).reviseis optional: the slug of an approved instrument this job corrects.askedsays what must change — a rule the page follows otherwise than the product’s reference, a line said another way — anddone_whenwhat it must then show. There is no matching: the page’s next version is written straight away, keeping everything else and every setting people chose, and goes to the owner as an extension does. Approved, it replaces the page for everyone who has it. A slug that is not an approved instrument of this product fails the job (PLAN D255).
A job is claimed only when it can be worked on. Aromba does not claim, and the job stays in the product’s queue, while any of these holds:
- no chat is paired, because a new page is shown to the owner first;
- the Warden is at Freeze or beyond;
- the assistant is off;
- the day’s AI budget the owner set is spent;
- no key is set;
- the product answered a
spenttoday withover, its word that what it gives Aromba for the day is spent: until the next UTC day (PLAN D245).
3. The instrument
One folder per instrument, in /var/lib/aromba/worker/instruments/<slug>/:
template.html— the page: one self-contained file.instrument.json— its slug, name and description; its kind; the product it serves; its input schema; the placeholders it reads; the product’s made-up example and the settings it was made with, for the preview; the asks it has answered, the last ten, which is what finds it next time; how often it was used; and where it came from: the job, the ask, the model, the cost, the seconds, when. Its approval is the SHA-256 of the page the owner approved. A page that no longer hashes to it is not approved, whatever else says so.
How a page gets its data. The product replaces each placeholder with that value as JSON, so the page’s script reads it as a JavaScript value:
<script>
const params = {{params}}; // the settings for one person, fitting input_schema
const state = {{state}}; // that person's data, as the product holds it
</script>
{{params}} is always there. The other placeholders are the ones the
brief names. The product escapes what it puts in, so nothing in the data
can close the script. Aromba’s preview does it the same way.
Files for the person (PLAN D250). A page hands the person its data as
a file through arombaFile, a script Aromba puts in every page that calls
it: csv, tsv, xlsx (a real workbook), text for TXT or MD, json,
pdf for an A4 document of the parts the page hands it — a title,
headings, text, tables and its own charts, each page drawn in the browser
and kept as an image, so any script comes out as it is shown (PLAN D274, D275)
— and png for an image of a card, drawn from the page’s own SVG (the
element or its markup) or canvas, or taken as a PNG data URL or Blob the
page drew itself (PLAN D262, D264); anything else is an error the page
can show, never a quiet nothing. A page that names arombaFile at all gets the kit, and a file
whose name has no extension gets its format’s (PLAN D270). The file is made in
the browser from what the page was given; nothing is loaded or sent. The
kit is Aromba’s, never a model’s: a copy a model wrote is replaced by it,
and every page goes out — to the product, and to the owner’s preview —
with the kit as it is now, so a fix to the kit reaches pages already
approved; the approval stays bound to the page as kept (PLAN D264).
print, the browser’s print of the page, stays for the pages that call
it. The product’s frame needs allow-downloads for the files and
allow-modals for print.
The firefighter (PLAN D279). A product may send, the same way, its
report of an error one of its users met: need and occasion
incident, its id I-<n>, with incident (its kind, api or crash,
the page, the user’s last action, the request’s method, path and status,
the message, the build, the browser, the time, and how many reports and
people joined it), lang (the user’s language), server_log (the api’s
lines for that request, and around it: other routes’ errors in the minute
before, the user’s refused calls, the page’s own last calls with their
answers — query strings, keys, addresses and ids taken out),
recent_deploys, known (the causes already found, newest first, each
{signature: {page, method, path, status, message}, verdict, cause}, so a
repeat is matched on its signature), and screenshot, a JPEG or PNG data
URL under 2 MB, with screenshot_masked. Aromba answers on POST incident
with {"job_id", "verdict", "user_message", "team_note", "confidence", "page"}: a verdict of bug, not_ours, user_error, transient,
stale_tab or unknown; one to three sentences for the user, in their
language, 400 characters at most, with no link, no code, no address and no
email; the cause and
the fix for the team, 2,000 characters at most; how sure it is, 0 to 1;
and, when a page would help that user now, the order in one sentence.
- Only by the owner’s rule for the occasion
incident(aromba intake auto <product> incident): without it the job fails saying so. - The words first. The screen is looked at only when they cannot
settle the verdict, and only a screenshot the product masked where it
was taken — every figure, size, address, email and form field painted
over,
screenshot_masked: true— ever: a model that reads pictures describes it without copying a figure, an address or a name, the redactor runs over the description, and the answer is reached from the words. The picture is scaled down to 1280 pixels wide and lives in memory for that one call. - The journal keeps the verdict, how sure it was, the seconds and the cost; never the user’s action, the message or the log.
The example the writer reads is cut to the first and last items of each long list, with a note of how long each is (PLAN D251): a page is written from the shape of the data and draws any length of it. The preview is filled with the whole example.
The example names only placeholders the brief names. A job whose example holds a key the brief does not name is handed back before any model is asked, saying which: the product fills only what its brief names, so no page could read it (PLAN D272). And it is at most 128 KB: the owner’s preview carries it whole, and a job whose example is bigger is handed back the same way, saying its size. A made-up example only has to show the data’s shape, because a page draws any length of the real data: about a thousand hourly candles fit (PLAN D276).
What a page may not do, checked in code before anything is kept:
- it loads nothing and sends nothing: no address but the SVG and XHTML
namespaces, and none of
fetch,XMLHttpRequest,WebSocket,EventSource,sendBeacon,import(,<form,<iframe,<object,<embed,<base,<link,<script src,@import, or a meta refresh; - it is 64 KB at most, UTF-8, HTML from its first character (a tag, after any blank lines), and reads every placeholder;
- the redactor finds nothing in it;
- its input schema is an object of simple properties — string, number, integer, boolean, array, object; required; enum; a range; a length — and the settings it was made with fit it.
These checks catch a model’s mistakes. The product’s no-network CSP is the
wall, and the owner’s review is the gate. When Aromba starts, every
approved page is checked again as it would go out, and one that fails
today’s page checks leaves the library for .retired/, with a journal line
saying why: no ask is answered with it again, and the next like it is
written anew (PLAN D268). The owner can take any page out the same way at
a root terminal: aromba intake retire <page> (PLAN D273). Jobs are built three at a time: one is claimed as soon as a place is
free, so a morning of a product’s users’ orders is not one queue behind a
four-minute page (PLAN D277).
4. What comes back
POST publish:
{"job_id": "J-17", "slug": "nutrition-board", "title": "Nutrition board",
"description": "The day's meals against a calorie goal, in the person's colours",
"kind": "page", "input_schema": {…}, "template": "<!doctype html>…",
"params": {"palette": ["#2a9d8f"], "goal_kcal": 2200},
"reused": false, "usd": 0.0021, "seconds": 48, "model": "z-ai/glm-5.3"}
The same instrument comes back whole for every job it answers. usd is
what the job cost, for the record; what is charged is spent, sent after
it. model is the model that wrote the page or answered from the library
(PLAN D229). added is what a next version added to the page it replaces
— “a heading and a second accent colour” — sent with the job that made it
and with no other (PLAN D239). params are
this job’s settings, and reused says it came from the library. A product
keeps one copy per slug and the params per person — otherwise every person
has their own and the cache is empty. A publish that fails is sent again at
the next pass. The exception is a job the product no longer has — expired
while the page waited for the owner, or never known. The product answers
that with 410 Gone, and the job ends and is not sent again; the
instrument stays approved in the library for the next ask like it. A
400 or 422 is the product refusing what it was sent: the job ends,
failed carries the first line of what the product answered, and the
page stays in the library (PLAN D258). No other answer means either. A 404, which is also what a wrong key gets, is
sent again once the key is right. spent follows each publish, and
failed any job that will not be done, with the reason in one line. A
job under the owner’s rule of no tap (PLAN D277) is one the product’s own
user waits on, so its error is a sentence that user can read — the
writer’s own, when it found the data this job lists cannot answer the ask
at all — and the reason as it is goes beside it as detail.
5. How a job is worked
- An exact repeat — same product, same kind, same words once normalised (case, spaces and punctuation) — is answered as it was the last time, with no model at all.
- Otherwise the matcher reads the job against the library: the
eight approved instruments for this product nearest by word overlap, with
their schemas. Colours, a heading, a period, units, a goal are settings,
never a reason for a new instrument. It answers from one
(
use_instrument, with this job’s settings, which must fit); extends one that does the job but whose settings cannot say what this ask wants (extend_instrument); looks at one’s page first (read_instrument); or says none of them does this job (none_fits). An extension is that page with something changed as a person looking at it would change it — a stop, a direction, a unit, a period; a page of its own that shows some of the same things — a risk card, a card to share, a file, a map — is none fits, however much of the data they share (PLAN D264). An empty library skips this step. The matcher is its own model inproviders.toml(PLAN D229). - An extension is the next version of that instrument: the writer gets
its page and settings and adds the ones this ask needs, keeping the job,
the look, the slug and every setting it has — the settings it was
approved with must fit the new schema. It changes the page by exact
edits, each finding a few whole lines as they are and putting the
changed ones in their place, and what no edit finds stays character for
character; a change to most of the page is written whole (PLAN D267). A
product’s correction (
revise) is made the same way. It waits beside the approved one as<slug>-next, and the approved one goes on answering. Approved, the new version takes the slug, and a product that upserts by slug gives it to everyone who has the old one. Not this removes the new version alone. - Only when none fits does the writer write one (
write_instrument): the page, its schema, and the settings for this ask, to a design brief — the answer first, one focal point, one memorable visual idea, charts that say their answer, the person’s own baseline, designed empty states, dark and light. The page that passes the checks in §3 goes back to the same model once more with a designer’s rubric, and the page it writes then is kept if it passes them too (PLAN D228). What fails the checks goes back with the reason, three times at most. The writer is the direction’s model at the tier the owner chose (§2,direction). - A new instrument or version goes to the owner as the page — a file in the chat, filled with its settings and the made-up example — with what was asked, what it cost and how long it took, why the library did not answer when it had something near, and [Approve] [Not this]. Approve binds the approval to the page’s hash and publishes. Not this deletes it and fails the job.
The build turn is a turn of its own, not the chat’s. It has its own short prompt, the brief, the job and the library, and the tools above. None of them reads this server. The chat’s tools are not in it, so an ask written to make the model say something about the machine has nothing to say it with.
Cost. A hit is one call on the matcher — a few seconds, a fraction
of a cent — or no call. A miss is two calls on the writer, the page and
its designer’s pass, paid once; an extension is one. After the owner approves, the library answers that ask for everybody
else. Every job keeps its model, tokens, cost and seconds, and the product
is told the cost. The owner’s daily AI budget in /settings covers these
turns as it covers questions, and is off unless they set it.
6. What the owner sees
- The chat: a new instrument, as in §5.4. Nothing for a hit. While a page is built, Telegram’s “typing…” (PLAN D247). A page that arrives while others wait carries “Approve all N”, one tap for every page that waits (PLAN D252).
/instruments: the library by product, the most used first, and what still waits for them.- The journal: a line for each claim, build, approval, publish and
failure, with the job’s id, the instrument, the seconds and the cost. It
never holds the ask itself, which is the product’s words about one person
(
05 §12). aromba intake: the products named, and whether a key is set.