avsmd
The obedient paperclip maximizer daemon that won't keep you up at night.
| Stars | Language | Updated | License |
|---|---|---|---|
| 0 | Rust | 2026-10-24 | Apache-2.0 |
The obedient paperclip maximizer. An agentic daemon that pursues the goals you give it, acts only within the limits you set, and has proof for everything it does.
avsm supervises ventures. A venture is a directory with a mandate: what it is
for, what number to watch, what it may do, what it may not. avsm keeps the
venture's process running, reads its metric on a cadence, and once a night lets
a model choose between doing nothing, taking an action the mandate permits, or
asking you. Every record in its ledger is measured by code, declared by a human,
or inferred by a model. An action is accepted only if a proof exists from
measured and declared facts under a mandate, and avsm why prints that proof.
Models propose. Code proves. Nothing a model says can authorize anything by itself, so leaving avsm running overnight is safe because harm is underivable, not because the model is good. You wake up to work done, not work to do.
Install
cargo install avsmdOr from source:
git clone https://github.com/4esv/avsmd
cd avsmd
cargo install --path .Two binaries: avsmd, the daemon, and avsm, which gives it commands. No
public listener: avsmd answers over a Unix socket in its home,
~/Library/Application Support/avsmd on macOS and $XDG_STATE_HOME/avsmd
(~/.local/state/avsmd) on Linux, or $AVSMD_HOME, or --home. A Unix socket
path is limited by the system, 103 bytes on macOS and 107 on Linux, and the
socket is sock in the home, so keep the home short: a longer one fails with
use a shorter --home or AVSMD_HOME, and avsmd -t and avsm doctor say so
before then.
Add a venture
Start the daemon. It runs in the foreground, so give it a terminal or a service (see Running it):
avsmdIn another terminal, take on the example venture:
avsm add examples/counterexamples/counter holds three files. report prints a number, one higher each
time it runs; the program that reads a venture's metric is always named
report, beside the mandate, unless the metric names a built-in one. reset puts the number back to zero. mandate.yaml says what to
do about it:
purpose: Keep the counter below ten.
metric: count
controller:
kind: threshold
above: 10
action: reset
cadence: 10s
permitted:
- name: reset
run: ./reset
cap: 60/hour
reversible: true
forbidden: [rm]Watch it work:
avsm status
avsm tailA duration (cadence, timeout, a cap's period) is a number and its unit:
10s, 5m, 2h, 1d. 1h30 is not one; write 90m.
The count rises by one every 10 seconds. Within two minutes it climbs past ten,
avsm runs reset, and then does so again about every two minutes: some 33 resets
an hour against a cap of 60, so the example stays inside its limit. The ledger
holds the reports, the action, and the proof that allowed it. Every line is one
record: its number, its time, its venture, who made it (measured, declared,
inferred), what it is, and a summary.
A venture directory may also hold run, a program avsm keeps alive and restarts
when it dies (restart: always, on-failure, or never in the mandate).
Actions are levers you install: a name, the command to run, how often it may
run, and whether it can be undone. avsm never runs an irreversible action on a
model's word. It asks instead; avsm asks lists the questions, avsm approve <number> and avsm deny <number> <reason> answer them. Write delegated: true
on an irreversible action to let it run unasked; avsm then records its intent
before and the outcome after. A lever runs apart from the daemon: while it
runs, commands are answered and the other ventures go on. One run may take ten
minutes, or what timeout: 30s on the lever says; past it the run is killed
and its outcome written as failed. Pausing the venture stops a lever still
running, and its outcome says so.
A lever may take content: takes: text, json, diff or file. A model can
then propose the text of an announcement or the diff of a change, and what it
wrote waits for you. avsm asks shows it under the question, avsm approve <number> --edit opens it in your editor so what you save is what runs, and
avsm propose <venture> <lever> <content> files a proposal of your own. The
editor is $VISUAL when it is set, else $EDITOR, else vi; it needs a
terminal, so with a terminal editor and no terminal --edit waits for nothing. Content
a model produced runs only after a human approves it, unless the mandate
delegates the lever.
avsm pause <venture> stops a venture and nothing but avsm resume <venture>
starts it again. avsm approve of a paused venture's ask is refused: the gate
would spend the approval without running it, so resume first; avsm deny is
answered whatever the state. A resume while a guard still reads past its bound
goes through, and says the guard will pause it again. There is no other steering. To change what a venture may do,
edit its mandate and run avsm add again; the old permissions end with the old
mandate.
A venture's name is the name of its directory, one word of letters, digits, _,
- and .; examples/counter is counter, and moving or renaming the
directory makes a new venture. Every program of a venture (report, run, the
levers) runs in that directory, so ./reset means the reset beside the
mandate. There is no remove: a venture is never deleted from the ledger, and
avsm pause <venture> is how you stop one. avsm doctor only notes a paused
venture whose directory has gone.
Guards, audits and plausibility
A guard is a reading of its own that the warden takes beside the metric, with
a bound. Past it, the guard pauses the venture (then: pause, the default) or
asks once (then: ask); either way no lever but stop runs until the reading is
back inside, whatever approves it. Every lever of the venture is refused, a
band's corrective one and a delegation too; only stop and the guard's own
unwind pass. A failed or implausible reading neither trips a guard nor clears
it. A guard may not read a lever's command or the metric's. A drawdown guard falls from the metric's peak. A guard may
declare an unwind: one permitted lever and the exact payload it takes, which
the kernel runs once per episode when the guard trips (and on later readings
past the bound until it has run ok), under the lever's cap, budget and check;
the same lever with any other payload stays refused:
guards:
- {name: spend, run: ./spend, above: 100}
- {name: errors, adapter: http.status https://example.com/health, above: 299, then: ask}
- {name: dd, drawdown: 15%, of: equity, unwind: {action: trade, payload: {side: sell, qty: 100000}}}
audits:
- {name: refunds, run: ./refunds, worse: up, tolerance: 2}
range: [0, 100000]
max_step: 5000An audit asks when the metric improves over its last window readings (5 unless
set) while the audit gets worse by more than its tolerance. The metric improves
going up unless the audit says better: down, as for unread mail. No model ever sees
an audit reading. A report outside range, a step past max_step, a flap, a
NaN or a failed report is kept, marked implausible, never acted on, and asks
once until a plausible report comes. A guard's or audit's failed reading holds
the venture implausible just as the metric's does; avsm status names the
reading that holds it, and what clears it. With max_step, the report right after an
implausible one is unconfirmed: kept and shown, never acted on, until the next
agrees with it within max_step; confirm: false on the mandate, a guard or an
audit acts on it at once, where waiting costs more than a misreading. Where a
metric moves faster one way than the other, max_rise and max_fall bound each
way, in place of max_step or beside it: stock that arrives by the pallet and
leaves by the day takes max_rise: 800 and max_fall: 300, and a fall to 0
from 398 is implausible though a delivery of 600 is not. Beside max_step, each
must be smaller than it and bounds its way tighter; a way bounded by neither is
unbounded. On a threshold, deadband: 2 acts once
and again only after the metric comes back past the bound by 2; hold: 3 acts
after three crossed reports in a row. A deadband means one action per
excursion, whether or not the lever then ran and even if the metric is still
past the bound after it; on a band, one for each side. Leave it off where one
run only nudges the metric and the lever should go on running, within its cap,
until the metric is back inside.
A report's number is its last non-empty line, and that line is the number
alone (count: 5 is a failed report). Text before it is dropped for the
metric and for a guard, whose record carries no note. For an audit the lines
above the number are the note, at most 2000 characters, which a payload check
may read and no model sees. A command that exits non-zero or prints no number
is a failed report, kept with a note that says why.
A reading is what a model may see that it could not otherwise: its command
prints lines and then a number, and the warden keeps only the lines that are
fields of the kind lines names (address, the one kind: text, an @, text,
no spaces or <>,;:, at most 254 characters), at most 50 of them, joined by
spaces. With audit:, a field is kept only when that audit's latest reading
names it, so the model learns who is waiting and never what they wrote. A
failed reading keeps no note at all. Its record, reading.<name>, is
inferred: it reads the open world, so it shows and never authorizes, and a
payload check that wants a sender still reads the audit:
audits:
- {name: customers, run: ./customers, worse: up, better: down}
readings:
- {name: waiting, run: ./waiting, lines: address, audit: customers}A band holds the metric between two bounds, with a lever for each side:
controller: {kind: band, low: 19, high: 24, below: heat, above: cool}Below low it runs below, above high it runs above, and in between it
runs neither. Each side acts as a threshold does, with the band's deadband
and hold; the deadband must be less than the band is wide. A lever for the
low side may only declare expects: up ... within ... and one for the high side
expects: down ... (see Expecting an effect, below); expects always carries
within, so expects: up alone is refused.
Doing something every hour
There is no schedule: a venture watches a number. To run backup every hour,
watch how long ago it last ran. report prints the age of a stamp the lever
touches, making the stamp first so a missing file is not a reading of 56 years:
#!/bin/sh
[ -e stamp ] || touch stamp
echo $(( $(date +%s) - $(date -r stamp +%s) ))purpose: Back up every hour.
metric: age
controller: {kind: threshold, above: 3600, action: backup}
cadence: 1m
permitted:
- {name: backup, run: ./backup && touch stamp, cap: 2/hour, reversible: true}
max_rise: 120Bound the rise only: the age falls to 0 after every run, and a max_step
would call that fall implausible.
Expecting an effect
A lever may say what a run should do to the metric:
permitted:
- {name: water, run: ./water, cap: 4/day, reversible: true, expects: up 5 within 3}
- {name: vent, run: ./vent, cap: 4/day, reversible: true, expects: down within 2}The syntax is up or down, then optionally a size (a number above 0), then
within and a count: up 5 within 3, down within 2. within counts
reports: the trusted, plausible readings of the metric that follow the run's
successful outcome, not minutes or runs. The count-th such report is compared
with the last reading before the run, net of what the metric was doing anyway.
avsm writes a record when the run missed, or showed its effect later than
within; a run that did what was expected writes nothing. Until the count-th
report the verdict is waiting. Without a size, any move the right way beyond the metric's own
noise is enough; with one, a move far off it is the wrong size. Absence of
evidence is not a miss. With fewer than 3 steps of history the metric's drift
is not known, and the run is not judged. A span of exact repeats of the reading
before the run may be a sensor that has not refreshed, as a cached df read
more often than it updates is. The run waits, and its lever with it, until a
reading differs or the metric's own history makes that many repeats in a row
less likely than 1%. A metric that has never changed is no evidence either way,
and its runs are not judged. With
revoke_after: n on a delegated lever, n misses in a row suspend the
delegation until you approve the ask it raises. A lever with expects also
gives the forecast and the capacity check an effect to start from before it
has run enough to be measured.
World models
world: ./world names a command that steps the venture's world: a JSON
state, a lever or none, a seed, the time and the last 200 trusted reports in;
a state, the metric and any guard readings out, bounded to 64 KiB, with
everything that is not a field dropped. Without one, the built-in baseline
resamples the metric's own drift and each lever's measured effect from the
ledger. With world: or calibration: 0.1, every report brings a one-step
prediction, an inferred record; the next trusted, plausible report resolves
it, and code scores it as a measured calibration.<model> report (CRPS and its
skill against persistence, Brier for guard events, 80% coverage). Below the
calibration: bar over the last 20 scores, or with fewer than 5, a ranking
from the model asks and never runs. A prediction never authorizes anything.
Letting a model choose
With controller: {kind: model} (or kind: classifier, which answers with one
word), once a night avsm shows a backend the mandate and the recent records and
takes its reply as a proposal, a question for you, or nothing. Backends are
named in config.yaml in the home; any API in the Anthropic or OpenAI style works,
and so does a local command, so a classifier you trained yourself fits:
nightly: is the local time of the machine avsmd runs on, as HH:MM, or
never; a change of daylight saving time moves it with the clock.
nightly: "03:00"
default: anthropic
backends:
anthropic: { kind: anthropic, model: claude-opus-5-5, key: anthropic }
deepseek: { kind: openai, url: https://api.deepseek.com/v1/chat/completions, model: deepseek-chat, key: deepseek }
local: { kind: openai, url: http://127.0.0.1:11434/v1/chat/completions, model: llama3 }
mine: { kind: command, run: /opt/mine/classify }Other keys in config.yaml, each optional (avsmd -T prints them all as
they stand): ratchet: 5 is the approvals of one lever that make self propose
delegating it, 0 for never; forecast: {days: 30, seeds: 16} (the default, which avsmd -T prints as a comment when you leave it out, and canary_asks: null for off) is how far and how
wide the forecasts at add, before a ratchet ask and in a decision packet look,
and seeds: 0 turns them off; canary_asks: {every: 20} writes a canary ask
after that many asks, and is off when absent; port: 2876 is the
tailnet port; adapters: says where stripe and github reach, each
{url: ...}, and how mail.send sends, mail: {command: "sendmail -t -i", from: you@example.com}; an anthropic backend also takes url and effort
(low to max), and a command backend offbox.
A decision packet's forecast is 7 days by 8 seeds until forecast: says
otherwise. A forecast runs apart from the kernel: commands are answered and
guards read while it runs, and the forecast at add is written when it is done.
It is fit to the readings no plausibility check doubts, held to the metric's
range:, and shown to the precision of its readings, at most two decimals; on
fewer than three lever-free steps of history, or with a spread the history
cannot account for, it says too little history to forecast instead of
numbers. A re-add of an unchanged mandate is not forecast again. avsmd reads config.yaml once, at start: after an edit, restart it,
and avsm add says config.yaml changed on disk; restart avsmd to apply it
while the file and the daemon differ.
Keys go in keys in the home, one name=value per line, readable by you alone.
Only the warden, the one process avsmd starts that may act, ever reads them; the
process that talks to models holds no keys and runs as nobody when avsmd runs
as root.
When the model cannot answer
If a reply cannot be used, or is bigger than avsm will read (256 KiB), avsm can
ask a second backend the same question. Name it with escalate: in
config.yaml. Before the call avsm writes a measured escalation record (from,
to, why); the answer comes back as inferred records that cite it, and the
escalation then gets a measured answer record: the records cited, or unusable. An
answer proposes or asks like any model reply and never authorizes. With no
escalate:, nothing changes.
examples/backends/claude is a ready second backend: it runs claude -p in
safe mode with no tools, on sonnet unless AVSM_ESCALATE_MODEL says
otherwise. It sends the prompt off the machine, so flag it offbox: true:
escalate: claude
backends:
claude: { kind: command, run: /path/to/avsm/examples/backends/claude, offbox: true }A backend that leaves the machine (every anthropic and openai one, and a
command flagged offbox: true) is asked only for a venture whose mandate says
escalate: offbox. Only you can add that line: edit mandate.yaml and run
avsm add. A mandate change proposed by self or the ratchet that adds it is
refused.
The request has a system and a user text; a command backend gets both as
one JSON object, {"backend", "system", "user"}, on standard input and prints
its reply, so a command that saves its input shows exactly what a model is
shown. The user text lists the permitted levers with their caps, the
children with their state and recent records, the readings, the forbidden
levers, the recent records and the open asks. The reply is one JSON object on
one line:
{"choice": "nothing", "reason": "all quiet"}
{"choice": "action", "action": "discount", "reason": "slow day"}
{"choice": "ask", "question": "Refund order 7?", "action": "refund"}
{"choices": [{"choice": "action", "action": "reply", "payload": "..."}, {"choice": "nothing"}]}A lever that takes content needs a payload field too: a string for text,
diff or file, an object for json. choices carries up to five
independent choices, each judged alone. A classifier answers one word
instead: nothing, ask, or a permitted lever. Before it chooses, the model
may look things up with {"choice": "know", "topic": "<venture or topic>"}
and {"choice": "why", "record": 12}, three times at most, and every look is
a record. A mandate may also permit research, a
lever that sends a question to a command you name under research: in the
config and keeps what comes back as inferred facts, each with its source. No
provider is built in; the command is the provider.
Running it
avsmd runs in the foreground and never forks. contrib/ holds a launchd agent
(io.aesv.avsmd.plist) and a systemd user unit (avsmd.service) that start it
at login and again if it dies; each file says how to install it. SIGTERM or
SIGINT stops it cleanly within 10 seconds: no new commands, the work in flight
finishes, a lever still running is sent SIGTERM and then SIGKILL if it lingers,
and its outcome is written before avsmd exits 0. A warden or roles process
that dies is started again, and a measured report of self's says so; a lever
it was running is asked about (did reset happen?), never run again blindly.
Every lever, report, guard, audit and reading command, and run leads a
process group of its own, and none outlives avsmd: the warden tells avsmd of
each group it starts, so when the warden dies avsmd ends what it left
running, and a stop, or the pause of its venture, ends the rest. Ending is
SIGTERM, then SIGKILL 7 seconds later. A guard, audit, reading or adapter
metric that takes longer than half a second is read apart in the warden, so
the tick goes on: the venture's report waits for its metric, and a guard,
audit or reading still running is written as not read until it is done.
A ledger that cannot be written stops everything that would write to it:
avsm status and avsm doctor say so, and avsmd tries again every 30 seconds.
avsmd -t # test the config and every adopted mandate, and say what each allows
avsmd -T # print the config in force
avsmd -f other.yaml # read another config
avsmd -d # verbose diagnostics on standard error
avsm doctor # check the home, config, ledger, mandates and daemon
avsm doctor --since 1234:0123456789abcdef # and that the ledger still holds this head
avsm doctor --cut 1234 # with avsmd stopped: back up, then cut just before #1234avsm doctor changes nothing. Each check it fails prints the command that
fixes it, and it exits with the worst code it found. Both binaries exit as
sysexits.h says: 64 for bad usage, 65 for input avsmd refused, 69 when no
avsmd is running, 78 for a config or mandate that does not hold. The 78 comes
from avsmd -t and avsm doctor, which read the files themselves, and from
avsmd for its config. avsmd starts beside a venture whose adopted mandate it
cannot read, since the others may be fine: it runs nothing of that venture,
names it on standard error, and avsm status cautions that it cannot run. A
mandate that avsm add is refused by the running avsmd is input it refused,
so avsm add exits 65 for it, and so does avsm propose for a proposal the
gate refuses and avsm tail 0. avsm add of a directory that is not there
exits 66, as avsm doctor does; a file given for its directory, or a directory
with mandate.yml for mandate.yaml, is 65 and says what to pass or rename. avsm doctor
exits 0 when every check passes, and otherwise with the worst code it found, in
this order: 65 a ledger that is not a database or whose chain is broken, 78 a
config or mandate that does not hold, 77 a permission missing, 73 a home that
is not a directory, 66 a home or mandate directory that is not there, 69 no
avsmd answering. A sound ledger and no daemon running is 69. avsm doctor --cut
exits 75 while avsmd runs on the home (stop it first), and doctor exits 64 for
bad usage: a --cut that is not a record number, a --since that is not an
anchor, or --remote. avsmd itself
exits 75 when another is already running on the same home, and names its pid:
each daemon holds a lock on avsmd.lock in the home for its life, so even with
its socket deleted a second one does not start, and avsm doctor says the
first is running but its socket is missing. Each second avsmd checks that the
lock file and the socket are still its own; one deleted or replaced is made
again. Should a second avsmd take the lock first, the first stops with exit
75 and says why, and from the moment the lock is another's neither it nor its
warden appends anything more, so the second raises no alarm over them.
avsm doctor fails when it finds more than one avsmd on the home, and names
the pids, and when the lock file is not a file. Records go to
standard output, diagnostics to standard error, and nothing is in color. A
standard output that cannot be written (closed, say) is said on standard
error and exits 74; one whose reader went away ends quietly.
avsm tail --since <number> prints every record after that one, for a script
that follows the ledger.
The ledger is ledger.db in the home, a SQLite file with a rollback journal
and synchronous=FULL, for tools of your own to read; open it read-only. Every
record is in the main file once it is written, so deleting ledger.db-journal
loses nothing. A ledger written in WAL mode by an earlier avsmd is moved to
this journal the first time avsmd opens it; no record is rewritten. One table, records: seq (the record
number), at (unix seconds), prev and hash (SHA-256 hex, the chain),
weight (always 1) and record, the record as JSON, whose kind (report,
proposal, action, ask and so on), claim (measured, declared or
inferred) and venture are always present. Triggers refuse every UPDATE
and DELETE, and every INSERT but the next record chained to the last: the
ledger is append-only. A record that still does not read stops avsmd, which
names it; avsm doctor --cut <number> copies the whole ledger to
ledger.db.before-cut-<number>-<time> beside it, cuts it just before that
record, keeping every record before it, and appends a declared record of the
cut. It refuses a ledger with no bad record, and a cut before record 1, unless you add
--force, and it never cuts at or before the record of an earlier cut.
What the chain proves, plainly: it catches corruption, an accidental edit and
anyone who can change the file without rehashing it. It does not catch a
rewrite by the file's owner, who can lift the triggers, change a record and
rehash every one after it, and it cannot tell that records were cut off the
end or that the file was put back from an older copy. An anchor kept off the
machine catches all three. The digest ends with the head of the chain as
head <number>:<hash>, the hash cut to its first 16 hex digits (64 bits); copy
it somewhere the owner of this machine cannot edit (mail it, paste it into a
ticket), and later avsm doctor --since <number>:<hash> fails with 65 unless
the ledger still holds exactly that hash at that number. An anchor covers the
chain up to its own number only: records after it are covered by the next
anchor you keep, so keep the latest digest's, and know that a cut or delete of
records after the last anchor you kept is not caught. --since takes the
digest's head line exactly as printed (head 42:0123456789abcdef; keep it off this machine...) as well as the bare <number>:<hash>. A bare hash of 16 to
64 hex digits is looked for anywhere in the ledger.
ledger.db and its journal are made readable by their owner alone (0600), and a
home avsmd creates is 0700. A home or ledger that already exists is left as it
is; avsm doctor warns, with the chmod, when the ledger is readable by others.
Simulating days
avsm simulate [--days 7] [--step 1h] [--approve all|none|mixed] [--start <unix seconds>] [--world <command>] [<venture directory>...]
runs ventures over simulated days in a home of their own, so pass --home <an empty directory>; it refuses your real home. The clock jumps one step at a
time, the nights pass, a scripted human answers asks (all approves every ask,
none answers none, mixed denies every third, edits text payloads and approves
the rest), and each record and each day's digest is printed as it is written.
No lever runs for real.
--world names a command that stands for the world. It runs before every step
with the simulated time in AVSM_NOW and the day in AVSM_DAY, and what it
prints is shown. Every lever run goes to it instead of the lever, with
AVSM_LEVER set to venture/lever (for example blog/post), AVSM_KEY, and
the payload on standard input; a world that fails stops the simulation. Readings
come from the world too: any line it prints as name=number or
venture/name=number is the reading of that metric, guard, audit or reading for
that step. A line name: a@b c@d (or venture/name: ...) gives an audit's or a
reading's lines, kept as the warden keeps a real one's: only address-shaped fields,
at most 254 characters each and 50 of them, an audit's as from <address> for
a payload check to read. A line name> text gives an audit's note as it is (the lines
above a real audit's number, at most 2000 characters). With --world, the world is
reality: a metric, guard, audit or reading it prints no line for this step reads as
missing, as a failed report does, never as the baseline, so a world injects a missing
sensor by printing nothing, a NaN by printing nan and an absurd one by printing the
number. The baseline is only for runs without --world. Without --world, a lever steps the venture's world model and
the reports come from its state; unless the mandate names a world:, that is a baseline learned from the
ledger, which replays the steps the venture's own readings have shown, so a
venture with little history gives a flat one. It starts from the ledger's last
reading, and a venture with none in the simulation's home reads nothing and
says so: with or without --world, no report, guard, audit or reading program
of a venture runs, since a program may change what it reads. A simulation exits 70 when it
cannot run or its world fails. The world is not avsm's and leaves no record; the
text it prints is escaped before it reaches your terminal.
Ventures from one file
A mandate alone is a venture when its metric and levers are built in. A metric
may be stripe.mrr, stripe.charges_today, github.open_issues owner/repo,
github.failed_ci owner/repo, imap.unread mail.example.com,
http.status https://example.com/health, file.number path or shell command.
A lever may name adapter: github.issue owner/repo, github.comment owner/repo,
github.pr owner/repo, mail.send you@example.com or http.post https://...
instead of a command. The warden runs them with the one key each is scoped to
(stripe, github, imap in the keys file); a model never sees one. A mandate
may say network: none or list the hosts the venture may reach, and the warden
holds its adapters to that.
Parents
A venture can own other ventures. The parent lists them under children:, each
child names it under parent:, and avsm add refuses any disagreement or loop.
A parent has four levers and no other: budget <child> <amount> sets what a
child may spend a day (levers say what one run costs with cost:), pause <child> stops it, resume <child> asks you each time, and threshold <child> above 25 moves the line a child's reports cross. Parents allocate; children
act. A parent never runs a child's lever, and its nightly call sees its
children's numbers and records, not what the world told them.
A child may be listed with terms, children: [{name: shop, priority: 2, floor: 3}, inbox]. floor (0 unless set) is the least budget the parent may set for
the child, and no approval lifts it. priority (0 unless set, higher is cut
last) refuses a cut to a child's budget while a lower-priority sibling has a
budget set above its own floor.
A parent may say allocation: {deadband: 2, hold: 5d, total: 6}: a budget
change under 2 is refused, so is moving a budget back inside five days, and
so is any budget that brings its children's budgets past 6 (a child with
none set counts at its floor). Approving the ask does not lift the total;
only you change it, in the mandate. A cut is never held when there is a
total, so a budget that should not have been set is undone at once.
Limits shared by every venture go in the config as quotas: {asks: 20/day, spend: 100/day, sends: 10/day}; avsm refuses what would pass one before the
gate is even asked, and avsm why shows the quota. A cap or quota window is
the 24 hours before now for day, 60 minutes for hour, 7 days for week:
6/day is six in any 24 hours, not six per calendar day, and no time zone
enters. A venture's daily budget is counted the same way. Only the built-in
metric stripe.charges_today counts from midnight UTC.
The asks avsm writes on its own count against the asks quota, except those
that carry a signal: a saturated or ineffective lever, an intent with no
outcome, a tripped guard that asks, an audit, an implausible report, a warning
of critical slowing down, a forecast that reaches a wall and a capacity
shortfall. Those are measured and sit outside it, so they cannot starve the
others. A canary ask and the ask for a run whose effect shows later than its
within do count; a canary ask is not written when the quota is used up.
Examples
Each of these is in examples/ and runs end to end with nothing but stubs.
counter: a number that climbs and one lever that puts it back. Three files, the smallest venture there is. Start here.github-repo: one file. Watches failed checks on the main branch, files issues and comments on its own, and asks before a pull request.stripe-store: one file. Reads monthly revenue through the Stripe key, researches freely, and asks before every mail to a customer.mail-inbox: counts unread mail over IMAP and drafts replies; every send is an ask, so nothing leaves without your word. Thewaitingreading shows the model the addresses with unread mail and nothing else of it.paper-trader: a book that follows a wandering price. Past ten percent of drawdown the built-in stop halts it, and only you can resume it.greenhouse: a thermometer that follows the clock and a vent that cools it. Above thirty degrees avsm opens the vent, and the proof names the reading that made it.portfolio: a parent over the store and the inbox. It moves budget between them, pauses one, and sees their records each night.
The morning after
avsm digestsays what the night did: actions, asks, cost, and one line per venture with its
metric and how it moved, and ends with the head of the chain to keep off the
machine (see the ledger, above). It is also written nightly as a record, so avsm know digest finds it later. avsm know <venture> lists what avsm currently holds
true about a venture, each fact with its age, who claimed it, and what it rests
on; avsm know <topic> does the same across ventures. When a venture's records
pile up, the nightly call summarizes the older ones into one record that cites
them; they leave the model's view and never leave the ledger.
As you approve the same thing again and again, avsm notices. After five
approvals of one lever with the same shape of content, the built-in self
venture proposes the mandate change that delegates it, with the five approvals
as evidence. Approve that and the mandate file is rewritten and taken on
afresh; the asks end.
Over Tailscale
With listen: tailscale and remote_users: [you@github] in the config, avsm
answers the same commands over your tailnet and nowhere else, and asks
tailscaled who is calling; avsm --remote host:2876 asks works from your laptop,
and the records say which login decided.
Why
avsm why 9prints the derivation behind record 9, one premise per line:
#9 2026-10-15T22:04:11Z counter measured action reset: done count is 0 again
counter: reset is allowed
mandate #2 declared adopted mandate e3f2c02fc1c2
permitted reset 60/hour reversible
report #8 measured count=11
threshold count above 10
cap 0 of 60/hour used
running counter is not pausedEverything in a proof is measured or declared. A model's proposal appears only
as the occasion, marked "authorizes nothing", because the rules are few and
fixed: no action without a mandate and a proof from measured or declared facts;
inferred records never authorize, they only propose or ask; stopping is always
allowed and resuming needs a declared record; irreversible actions default to
asks; content a model produced is inferred until a human approves it; parents
allocate and children act; anything read from the open world is inferred,
checked against the record's shape, bounded in size, and stripped of everything
else; a change to a mandate is a proposal that only a declared decision adopts;
the ledger is append-only and hash-chained; only the warden holds credentials;
and the built-in self venture, which checks the chain and the gate every ten
minutes and stops everything if either fails, can never edit its own mandate,
merge to main, or touch keys.
Plainly: run and script levers are not sandboxed. They are your programs, run
as you wrote them. Only adapters and payload levers are constrained by the
warden, which hands them their one key and holds them to the mandate's network
list. network: none cuts run off from the network where Linux allows it,
and avsm status says so when it cannot.
A run lever, a script lever and a report program are trusted code: they
run with your authority, as your user, and anything you can do, they can do,
the ledger and the socket included. avsmd cannot prevent that; it detects the
ways such a program could pass for you:
- A record avsmd did not write. While it runs, avsmd is the ledger's only
writer, and it knows every record it and its warden appended. Any other
record, however well chained, is never acted on: self writes a measured
foreignalarm citing it (an approval among them is spent for good), pauses everything, and says so on stderr, inavsm statusand inavsm doctor(exit 65). Read the records it names withavsm why, thenavsm resume self. - A command from a process avsmd started.
approve,deny,resume,addandproposeare refused and logged when they come from a lever, a report, a run or a role: anything whose parent chain leads back to avsmd, or anything in a process group or session avsmd made, alive or gone within the last minute. Every lever, report, guard and run starts in a session of its own, so a program that double-forks (( ( avsm approve 12 ) & ) &) and leaves the tree is still refused: it keeps the session, and the group too unless it changes it. On Linux avsmd and its warden are also child subreapers (PR_SET_CHILD_SUBREAPER), so what a program leaves behind is reparented to them and stays in the tree. Reading commands andpausestay open to them. Where the system cannot say who the peer is, the command is allowed.
What it does not detect: on macOS, a program that calls setsid itself and
then talks to the socket (it is in no group or session of avsmd's, and its
parent is the first process); and anything written while avsmd is stopped.
The anchor kept off the machine covers the second.
Nothing avsmd starts outlives it. Each group is written down in the home
(groups/) with its leader's start time and a mark in its environment. If
avsmd is killed outright, its warden sees the pipe from it close, ends its
own runs (their outcomes say killed: avsmd died) and ends the daemon's
reports and runs too; if all of them are killed at once, the next start ends
whatever is left, once it has checked by the start time, the mark or the
directory that each group is still the one written down. A program that
calls setsid itself leaves the group avsmd ends, and outlives a stop or a
pause.
Only the first answer to an ask counts; a later one is kept and authorizes nothing. An approval refused for any reason (a pause, a cap, a budget, a quota) is spent, and the next ask comes when the situation allows.
Where this comes from
avsm is built on Stafford Beer's Viable System Model, the cybernetics of
organisations that stay alive. A venture's run is the operation; the cadence,
the caps and the gate are the regulation that keeps it inside its mandate; the
nightly model call is the part that looks outward and ahead; the mandate is
policy, the one place a human steers; the parent is the level above, allocating
and never operating; and the self venture is the alarm that bypasses
everything when the system itself is in danger. None of that needs knowing to
use avsm. If you want it anyway, read Beer's Brain of the Firm.