Aromba — Security
Aromba runs on the user’s server with real permissions and talks to an AI model. This document is the contract that makes that acceptable. Every rule here is enforced by code, not by prompts, and none of it depends on the user understanding it. Anything not listed as allowed is forbidden.
1. Threat model
What we defend against, in order of likelihood:
- The AI does something wrong — hallucinated command, bad advice, over-eager cleanup.
- Prompt injection — a log line, a web page, or a file contains text that tries to steer the AI.
- A user who taps “yes” without reading — most users. The design must keep them safe anyway.
- Telegram account compromise.
- Leaked AI key.
- Compromised Aromba update — our release channel is attacked, or we ship a bad version.
- Compromised aro.mba relay.
Not in scope: an attacker who already has root on the user’s server. Aromba can’t protect against that; it only must not make it easier.
2. Two processes: Worker and Warden
Aromba is two processes that do not trust each other.
| Worker | Warden | |
|---|---|---|
| Does | chores, bot, assistant, health collector | executes privileged actions, watches the Worker, enforces policy |
| Runs as | user aromba, no sudo, no privileges of its own | root, its own systemd service; minimal code, no LLM, no network except its own channel to the relay (ladder alerts out, freeze signal in) |
| Can | ask the Warden to do things over a local socket | refuse, execute inside a cage, run for a core chore the few commands of section 5’s second table outside it, kill the Worker, lock itself down |
| Can touch the other | never | at any time |
The Worker holds no power. Every command that changes the system — every red action, every privileged call, every chore run — is a request to the Warden. The Worker’s own code never imports os/exec, and CI checks every package of ours in the Worker binary for it; the SQLite library’s C-runtime shim (modernc.org/libc) imports it transitively for a system(3) emulation SQLite never calls. The Warden checks the policy (section 5), the approval (section 7), the cage (section 3), and then runs it. If the model, an injection, or a bug fully controls the Worker, they control a process that can only ask.
An assistant tool a skill brought runs through the Warden too, and is the smallest of these paths: a script in a cage with nothing writable at all, no approval and no PIN, because it cannot change anything. What the model passes it arrives as environment variables and never as arguments; the skill must be installed and on, the script must be the bytes it brought, the host must be able to build a cage at all, and the answer is capped and redacted. A tool that needs to write is a chore (PLAN D143).
The chores folder is root’s too, and the Worker cannot write a file into it. A chore the assistant wrote is saved by the Warden, which decides whether it may be: red, off, author: mine, a header that parses, a name that is lower-case letters, digits and dashes and can only be a file in mine/, and a name that is not already taken. None of those is a prompt’s word — a model that writes safety: green is refused, as is one that writes a path where a name goes. Saving changes nothing on the server: the chore is off until its owner turns it on, running it asks what every red chore asks, and lowering its colour is the PIN’s (PLAN 3.9).
The Warden’s binary, config and policy files are root-owned and unreadable to the Worker. The Worker reaches the Warden only over a root-owned Unix socket, and the Warden serves a request only if the peer’s uid (SO_PEERCRED) is the aromba user; terminal commands (aromba panic, resume, fence, pin) use a second socket that accepts only uid 0. aromba key and aromba calendar go to the Worker’s own socket, which answers them only to uid 0, so the Worker writes its own secrets and root never writes into the Worker’s folder (PLAN D101). The Warden verifies its own signature, the Worker’s, and that policy.toml matches the signed release, at start and hourly.
3. The cage: touches is enforced, not documented
Every chore runs in an isolated namespace (bubblewrap or equivalent) where only the paths listed in its touches header are writable; everything else is read-only, and the paths in section 4 are not mounted at all. A chore that declares “Docker cache” and writes to /home/james/clients gets a permission error, is killed, and the event is logged. What is made writable is the folder the Warden checked, not whatever its name points at a moment later: each touches path is opened with no symbolic link followed and mounted into the cage from that open handle, and below a user’s home no link is followed at all — a cache that is a link, or sits behind one, is left out of the run, which says so. The folders under a home are their owner’s; root’s cleanup is not theirs to aim.
Applies to every chore: core, community, assistant, run, and everything a skill brings. Core chores are caged too so we catch our own mistakes. A skill’s permissions (paths, root commands, the network, network hosts) are declared in its manifest, shown before install, and are the maximum any of its chores or tools can get; a skill cannot widen them after install — a version that names one more writable path or one more root command is installed only with --wider, and the PIN again. A declared root command is one of the binaries the chore-action table below already runs, and a red chore of that skill may ask for it exactly as a core chore does, arguments judged by the same grammar (PLAN D134). A skill’s chores run with no network at all unless its manifest says network = true (PLAN D101); every other chore keeps the host’s (D46). No chore can make a Unix socket: a connect to a daemon’s socket is a way out that a read-only mount does not stop (root’s tmux in /tmp, LXD’s under /var/snap, an abstract one for a chore that keeps the network), so every cage runs under a seccomp filter that refuses socket() for AF_UNIX, a Unix socketpair() that is not a stream or seqpacket pair, io_uring, and any call made for another architecture — wherever a socket is bound, and whenever (PLAN D106). A stream or seqpacket pair, which reaches nothing outside itself, still works. A refused call answers “Operation not permitted”, which is not a cage violation. A cage whose filter cannot be loaded is not built, and its chore does not run. The mail queues are fenced as data (/var/spool/postfix and its kin).
No namespaces, no cage, almost nothing runs. Some hosts (LXC guests, some container VPSes) cannot create namespaces. There the Warden runs only the green chores of the system skill and refuses every yellow, red, skill and mine/ chore: “This server has no isolation. I only run green system chores here.” There is no setting and no button to run uncaged; the Trust page shows isolation: none.
Damage cap: no chore may change more than 1,000 files or 20 GB in one run unless the header declares a higher max_changes and the user confirmed it. The Warden checks the cap every 5 seconds during the run and kills at it; in those 5 seconds a chore can exceed it. The exact count is taken after the run and journaled. SECURITY.md says this in the same words.
4. Files and fences
Never readable, never sent anywhere, redacted if they appear in any output:
/etc/aromba/worker/secrets.toml(the AI key) andrelay.tomlbeside it (the relay identity, apart since 3.1 so a wipe of the key leaves the chat),calendars.toml(the private addresses of the person’s calendars, PLAN D101) andintake.toml(the key of each product that hands Aromba work,15-instruments.md) — one list in the policy, which the cage hides from every chore (PLAN D211) —/var/lib/aromba/worker/state.db(the Worker’s own state, the user’s calendar among it — PLAN D107),~/.ssh/*,~/.aws,~/.config/gcloud,~/.docker/config.json,~/.netrc,~/.gnupg- any file named
.env*,*.pem,*.key,id_*,*secret*,*token*,*credential* - content matching key patterns (
sk-,ghp_,AKIA,-----BEGIN, long base64 near “key”/“token”/“password”)
Auto-fence. At first scan and weekly, the Warden finds data directories on its own and fences them (no read, no write, not mounted in any cage): database data dirs (Postgres, MySQL, Mongo, Redis dumps, SQLite files over 10 MB), uploads, storage, media, data, backups, mail spools, git repos with uncommitted changes. The user sees the fence list on the dashboard and can add with aromba fence <path>, or from the chat: a path named when Aromba asks what it must never touch is offered as a fence, and the tap adds it (PLAN D225). Adding only takes away what Aromba may do, so it needs no PIN; removing a fence needs the terminal.
Honeypots. The Warden plants decoy secrets in places nothing legitimate reads: ~/.aws/credentials.bak, /etc/<app>/.env.production, a fake key file in the web root. Any Aromba process opening one triggers step 3 of the ladder immediately (section 8). Decoys are regenerated on each install and never listed anywhere the Worker can see.
Redaction happens in the Warden before any tool result reaches the model or Telegram. What only the Worker holds — a connected calendar’s addresses and titles, and the titles of the user’s own calendar items (PLAN D101, D107) — the Worker redacts before it leaves the Worker. Redaction counts are logged.
5. Command policy
Three lists, checked by the Warden on every request:
- Allow (runs without asking): read-only commands only —
df,du,ls,ps,free,uptime,ss,hostname,nproc,docker ps|images|system df,systemctl status|list-units|--failed,journalctl,apt list --upgradable,timedatectl show,fail2ban-client status,caton a short list of system files. Each has its own argument grammar — the flags it accepts and what each position may hold — and anything else in the arguments is refused. Fixed list, not user-extendable in phase 3. - Ask (button): everything else.
- Deny (never, even with a button):
rm -rf /,rm -rf ~,rm -rf /*and variants;ddto a block device;mkfs;curl … | sh,wget … | sh; writes to/etc/sudoers*,/etc/aromba/*,/etc/ssh/sshd_config,authorized_keys;chmod -R 777;shutdown/rebootoutside the reboot chore;iptables -F;history -c; any command naming a secret or fenced path; anything piping secrets out.
Deny hits are logged, reported (“The assistant proposed a blocked command: …”), and count toward the ladder.
The Warden never runs a shell: an allowed request becomes a fixed binary path and argument list, with an empty environment, / as the working directory, a timeout and a cap on output bytes. Paths in arguments must be absolute and free of .., are resolved through every symlink, and the result is checked against the secret and fence rules — so a link from /var/log into ~/.ssh is refused like ~/.ssh itself. A listed binary asked to do something that is not a read (docker restart, systemctl restart, apt install) gets “ask”, not “deny”: it needs a button.
A chore has no button but its own approval, and the cage hides the sockets systemd and apt live behind. So a chore asks: read-only questions in its header (asks:), which the Warden answers from this same allow list before the run, and do= result lines after a clean real run, which the Warden judges against a second, shorter table it also renders here — systemctl restart <unit>, timedatectl set-ntp true, apt-get install …, and Docker’s leftovers, docker image prune -f and docker builder prune -f (PLAN D230: dangling images and the unused build cache, never -a, a volume or a container) — and runs outside the cage, one journal line and one receipt line each. Only a chore we ship may ask at all — a request is a write no button showed, so a community chore, the user’s own and a skill’s get none of this until a header can declare it to the user. A red chore may ask for any of them; a green chore only for the two time sync needs; a yellow one for none. A restart names one .service, never one whose restart is a reboot, a rescue shell, the bus or Aromba itself; apt-get only upgrades packages dpkg lists as installed here, or installs one of a short named list, never removes, never prompts. A request the deny list or the secret rules stop counts toward the ladder like the Worker’s would; from Freeze up nothing more is run. What apt installs is not snapshotted: the two chores that ask for it say on their button that Aromba cannot undo it, and from 3.4 on that needs the PIN like every action that cannot be undone (section 7).
The lists are compiled into the Warden. /etc/aromba/warden/policy.toml is the Warden’s own rendering of them, written at install, root-owned, part of the integrity check (section 8), and shown verbatim on the dashboard’s Trust page and in SECURITY.md; the Warden never reads it back — editing it changes nothing and trips the check. Only the two tables — the read-only allow list and the chore actions — are compiled in: fences, the PIN hash, prod mode, color overrides and the permissions of installed skills are data under /etc/aromba/warden/, root-owned, written only by the Warden, and covered by the same integrity check, which the Warden re-baselines after its own writes. aromba trust <chore> widens a chore’s rights only within the bounds of its skill’s manifest, never the allow list.
6. Prompt injection
- Everything the model reads is wrapped and labeled as data. Soft defense.
- Hard defense: the model cannot execute anything. It proposes; the Warden and the user execute. Injected text can at most produce a button the user sees with the exact command on it.
- The assistant never auto-runs a command composed from text it just read: an argument that appeared in a tool result in the same turn and is an address — a URL, a host name, an IP with or without a port — is refused, and the turn is told why. A unit, a container or a path read the same way is not refused: reading the log of a container the tools just named is what the tools are for, and the Warden checks the shape of every argument regardless. Enforced in the Worker, before the Warden is asked.
web()is off by default; when on, only a documentation-domain allowlist.- A product’s job (
15-instruments.md) is text another program wrote, about what one of its people wants, and it starts a model turn with no tap. That turn is built so there is nothing to inject into: it has none of the chat’s tools — nothing reads this server — only the library of instruments and a way to write one, which is checked in code, kept as data and put in front of the owner before it reaches anyone (PLAN D210) — or, for the occasions the owner names at a root terminal as their own rule (aromba intake auto, PLAN D277), sent to the product’s one user who asked as soon as it passes those checks, as a page of its own that replaces nothing and answers no one else until the owner adds it to the library, and shown to the owner after. The product can name no occasion itself. - What a group says to itself, where Aromba is an admin and reads along (
PLAND219 E), is text anyone in that group wrote. It is redacted before it is kept, kept in memory only — never on disk, never in the journal, and the relay carries it and keeps none — and reaches the model only in front of a question asked in that group, labeled as people’s words to each other and never an instruction. It changes nothing a turn may do: the tools are the same read-only ones, and anything that acts is a button. - A transcript is text somebody spoke, and so it is exactly the text above: it reaches the assistant as a question the person asked, with no more standing than a typed one, and is quoted back in the chat before anything is done with it (
PLAND144).
7. Approvals, Telegram, and plain language
-
One
chat_idper server. Messages from any other chat are dropped before parsing. -
A red approval is valid for one run and 24 hours, bound to the exact command and params shown on the button.
-
Every red button is written for someone who won’t read docs: what it does, how long, what it cannot do — “Restart the website — about 5 seconds offline. Deletes nothing.” Under it, one line: what could go wrong, and whether it can be undone. If it can’t be undone, the button says so and needs the PIN.
-
PIN (4–6 digits, set with
aromba pin) is required for: anything irreversible, anything touching a fenced path’s neighbor (a run that may write the folder a fence sits in — the cage keeps the fence itself out of reach either way, but writing what holds it is the person’s own call; a sibling is not a neighbour), disabling prod mode, lowering a chore’s color. The PIN is entered on an inline keypad in Telegram: digits travel as callback data, nothing is typed into the chat, nothing is left there to delete. The Worker forwards the digits to the Warden, which compares the hash; the Worker never stores them. What comes back from a right PIN is a ticket the Warden holds — good once, gone in two minutes, forgotten on a restart — and the action is run against that ticket, so a Worker that claims a PIN it never had carries nothing. The digits are kept only as an Argon2id hash and its salt, in the Warden’s checked state folder; three wrong tries are answered and every one after that sets a wait that doubles to an hour and survives a restart, so a million combinations cost real time. A run that cannot be undone says so on its own button before it is tapped, rather than failing afterwards. More than four wrong PINs in an hour climbs the ladder — a wrong PIN is a thumb, a run of them is a chat in hands that never had it — and every PIN asked for or got wrong is a line in the next morning’s receipt, so a chat someone else is holding is visible to a person who never opened it. -
The code from an authenticator app stands exactly where the PIN stands (
PLAND190, D192): the same keypad, the same ticket, the same lockout counter, the same ladder. The Warden reads six digits as a PIN and, failing that, as a code; either one opens the same door and no caller knows there are two. It is six digits from a shared secret and the clock, a new one every thirty seconds (RFC 6238), so it works on a phone with no signal — which is the case it is here for. A code is spent when it is used, so the same six digits inside their ninety seconds do not open a second door. -
What it buys is that it can be set up from the chat, and that is the only thing it buys. The PIN needs a root terminal, and on a server where nobody ever opened one, every action in the list above is refused to everybody — that is a wall, not a lock.
/securityshows both, and the code is offered once on the first morning after pairing (09 §3’s first-week card), because a person only looks for a setting they already know they want. Setting one up sends a QR code, with the key written under it and theotpauth://line beside that: Telegram on a laptop and a phone in the hand is the shape authenticator apps are built around, and the key serves the phone-only chat, a second device, and putting the code on a new phone later.aromba totp -newdoes the same at a root terminal. Nothing is stored until six digits come back proving an app took it. -
Who may set one up. A server with a PIN must give the PIN first — you prove the factor you have before adding another form of it. A server with no PIN need not, because there is nothing yet to prove, and the chat is told in as many words that the code is now the only thing standing between it and what cannot be undone. Turning a code off always asks for the code or the PIN, from the chat or the dashboard, or it would be a code a stolen chat could replace with its own;
aromba totp -offat a root terminal asks for neither, and is how a person who lost the phone gets back in. -
On the same phone as Telegram it is a second screen, not a second factor (
PLAND116, which said so and still stands). It earns the name only when the code lives on another device, which is why the secret is written out for one. It is offered for the terminal it removes, and the chat does not pretend otherwise. -
Bot token lives on the relay, never on user servers. Each server holds only its own relay token.
-
The terminal is the root of trust:
aromba panic,aromba pair,aromba resume, fence removal, and ladder steps 3–4 recovery happen there, never from Telegram.
8. The ladder
The Warden watches Aromba, not the server, once a minute:
| Signal | Threshold |
|---|---|
| Commands requested by the Worker | > 100 in 10 min, one step per crossing (PLAN D105) |
| Deny-list hits | 3 in an hour |
| Secret or fenced path access attempts | > 2 |
| Outbound connection | any host not in network.toml |
| Files changed after a chore | anything outside its touches |
| Integrity | binaries, config, policy.toml, or Warden state changed |
| Approvals | a red action run without a fresh, matching approval |
| Honeypot | any read |
Response goes up a ladder; each step messages the user:
- Slow — Worker rate-limited to one command per minute. Message: “Unusual activity, I’ve slowed down. Was that you?” [Yes, carry on] [Freeze]
- Freeze — scheduler and assistant stopped, no chores run, chat stays alive. [It’s me, resume] [Keep frozen]
- Cut — the Warden answers the Worker nothing but the read-only allow list, and wipes the AI key from memory and disk; the Worker is read-only. Root’s own commands at the terminal still work — root at the terminal is the root of trust (§1), and fencing a path again is how a person repairs an integrity finding. Resume only with
aromba resumein the terminal; a resume from Cut restarts the Worker, which takes the relay’s key back on its next connection (PLAND271). - Disarm — Warden disables its own service and stops. What’s left is a Worker with no rights. Reinstall to recover.
Honeypot reads, integrity failures, and outbound to unknown hosts jump straight to step 3. Everything else climbs one step per trigger and climbs down after 24 quiet hours. /panic from Telegram = step 3; aromba panic from the terminal = step 4.
A false positive that climbs a step costs the user one tap; one that jumps to Cut costs a trip to the terminal, which is the price of a Cut nobody but root can undo. True positives cost the attacker the server. The honeypot cannot tell who opened a decoy, so anything run as root that reads the homes — a recursive search, a backup of /home — trips it too; that is kept on purpose (PLAN D214).
9. Safe by default, without understanding
- Prod mode turns on by itself when the first scan sees a production server: public domain answering HTTP, database with data, sustained traffic, or a running app older than 30 days. In prod mode: every chore is red, nothing is deleted (quarantine never purges), the assistant only proposes — except a watcher that reads and changes nothing (green,
verb: watch, atouchesline that names no path), which runs by itself as it does anywhere else. Prod mode guards against change; every alert Aromba raises comes from a watcher, and stopping them left the machines prod mode is for with no alerts at all (PLAND206). Turning it off is a PIN action — or the authenticator code, which stands in the same place — and shows the consequences in plain words. - Probation week. For the first 7 days after install only green chores can run, whatever the user enables. Yellow and red unlock after 7 days of clean runs. Enabling them earlier answers “in 4 days”.
- Dry-run first, always. Every chore’s first real run is preceded by a dry run whose result the user saw.
- Undo everywhere. ✅ Any red action that changes files snapshots them first, and a run that changed something comes back with [Undo] under it in the chat. The snapshot is the Warden’s, in its own checked folder, and the restore works through handles on the folder above each path, so a link planted after the snapshot carries nothing. Seven days. A run that changed nothing, and a dry run, carry no button: there is nothing to put back.
- Auto-fence and honeypots (section 4) need no user action.
10. Network
-
Outbound HTTPS only, no listening ports. Telegram through the aro.mba relay over an outbound WebSocket.
-
/etc/aromba/worker/network.tomllists allowed hosts:relay.aro.mba,aro.mba(where releases are published, PLAN D98), the chosen AI provider, distro mirrors, doc domains if enabled, the host of each calendar address the user saved, while thecalendarskill is on (PLAN D101), and the loopback address of each product named witharomba intake add— a product on this machine, which the Worker asks for work and never anything else (15-instruments.md, PLAN D210). Enforced in the Worker and, when nftables is available, by a per-user outbound rule the installer offers (not built yet: QUESTIONS 41, D77). It also carries the relay’s addresses for the day DNS fails: the daemon then dials an address directly but still verifies the relay’s certificate against the namerelay.aro.mba, never against the address and never without verification. The list is written at install; each signed release refreshes only the relay’s addresses in it, never the hosts, so a release can never widen what a server may reach without its owner seeing it (PLAN D103). -
The relay stores only: server ID, chat ID, relay token, messages in transit (deleted on delivery, 24 h max) — plus, from phase 4, what
08-dashboard.mdlists. What phase 2 added, each by the owner’s opt-in or for the owner’s own account: incident reports and nightly totals under the hash of a random install id, never beside a server or an address (section 11); for an account that signed in on the founders page, its sessions by the hash of their cookie, and the payments seen to its deposit address — which does tie a Telegram account to a chain address, and is kept for as long as the seat is (11-money.md§6). -
What a capability skill fetches (
13-skills.md§2,PLAND144) comes from the same host the releases do, over the same allowlist, and is trusted for the same reason nothing else about it is: the SHA-256 of each file is in the skill’s own manifest, which is signed, and the Warden checks the bytes against it before keeping them. A file that does not match is not kept and the Worker’s copy is dropped. The Worker downloads because the Worker is the half with a network; it never writes into the Warden’s folder. -
Voice (the
voiceskill) adds no host and no account. Telegram holds the audio, the relay fetches it and keeps none of it, and the transcription happens on the user’s own server: the Worker decodes the Opus in Go — a voice message is bytes a stranger chose, and a bounds check in Go is a panic where in C++ it is a memory bug — and the Warden runs whisper on the resulting WAV in a cage with no network at all, nothing writable but one folder holding that one file, emptied after, and the same seccomp filter every cage loads. Nothing said into the server leaves it, and there is no key anywhere to leave it with.
11. Updates and our own liability
- Releases are signed; the Warden verifies signatures before any update; the installer verifies the release manifest’s signature against the release key it carries, and each download against the manifest’s hash (the files are served from aro.mba; the signature, not the host, is what is trusted). Reproducible builds, public workflow. In practice: a release is two tarballs and one manifest naming their hashes, the manifest signed with the release key. The installer checks the manifest with minisign and the tarball’s hash before anything moves; the Worker downloads the same files into its own folder and asks; the Warden — the only writer — checks the signature against the key compiled into it and the hash against the manifest, unpacks into
/opt/aromba/releases/<version>/, swaps onecurrentlink, keepsprevious, rewrites its integrity baseline and restarts. Three failed starts roll the swap back (14-failure.md§2). The Worker never touches a binary. - Staged rollout. A release goes to 1% of servers, then 10% after 24 h, then all. A rise in ladder events on the current stage halts the rollout automatically. A server the latest release has not reached takes the newest patch that has, so releases a few hours apart never leave a server where it was (
PLAND265). - Remote freeze. aro.mba can send one signed signal to all Wardens: freeze (ladder step 2). Never anything else. This is how we stop a bad release in a minute. The Warden accepts only this signal, only signed with the freeze key — a dedicated key kept offline, never the release key. A third offline key signs key rotations and nothing else. All three public keys are built into the Warden, which fetches the signal itself — one HTTPS long-poll held up to five minutes, not a second WebSocket — never through the Worker.
- Incident reports go to us only if the user opted in, and contain only: ladder step, trigger type, Aromba version. No paths, no commands, no server identity beyond a random install ID. The opt-in is
aromba reports onin a root terminal, and nothing else: the flag lives where the Worker cannot write, the Warden sends the report itself with no credential, and the relay keeps the install ID hashed. - Auto-update on for patch versions, off for minor and major (bot asks).
mine/and fences are never touched by updates.
12. Logging and audit
/var/lib/aromba/journal/— append-only, one line per action: time, actor (chore / assistant / user / warden), command or tool, result, redaction count, cage violations. The Warden owns the file (root:aromba,0640, openedO_APPEND, fsync on every line, rotated by the Warden alone); the Worker adds its lines through a Warden request and can only read.aromba log ai— what the assistant did, last 7 days: every tool it ran with its arguments, how many steps a turn took, what it cost, which model answered, and the week’s total. Never the question and never the answer: the journal holds the shape of a turn and nothing of its content, because a question is the user’s own words and may carry anything — the same reason a calendar item’s title never reaches a journal line (PLAND107).- Logs rotate at 30 days locally. Nothing leaves the server unless the dashboard sync is on, and then only what
08-dashboard.mdlists.
13. What we tell users
A SECURITY.md in the repo with sections 2, 3, 8 and 9 in plain words, policy.toml verbatim, and a one-line summary at the top: “Aromba starts with almost no rights, earns them over a week, and loses them the moment it behaves strangely. The AI proposes; a separate guard executes; you tap.” Plus a disclosure address and a promise to publish every security fix in the changelog.
What review has been done, and what has not (PLAN D148). Twenty-seven rounds of adversarial review have been run against this document and the Warden’s code, each round’s findings fixed before the next: v0.1.2 to v0.1.9 are named for them, and round 27 came back with no High or Medium finding (D104). An external review by somebody who breaks things for a living has not been done, and this launch does not wait for it. It is on the list of what happens after, and until it happens SECURITY.md says so in the same words.