One command carries synthetic EEG through every AxonOS component, withdraws consent in the middle of the session, and counts what still reaches the application. The count is zero. Building it turned up the gap that would have made it twelve.
Denis Yermakou · The AxonOS Project · October 2026
Brain–computer interfaces are leaving the lab. Headsets read attention, implants restore speech, and consumer devices advertise focus scores. The law has started to follow. Colorado and California added neural data to the sensitive categories of their privacy statutes in 2024. Chile amended its constitution in 2021 to protect brain activity and the information that comes from it. The details differ, but the direction is the same everywhere: a person can say stop, and a system has to honour it.
Every privacy policy in neurotechnology already promises that. Hardly any of them says how anyone could check.
The question is harder than it sounds, because a BCI is a chain, not a single point. Electrodes feed an analogue front end. A converter turns voltages into codes. Firmware buffers them, a transport moves them, filters condition them, feature extractors reduce them and a classifier turns the result into something an application can act on. A withdrawal of consent arrives at one point in that chain, while data is in flight at all the others.
So "does the consent module flip its flag?" is not the useful question. Of course it flips its flag. The useful question is this:
After the flag flips, does anything — raw samples, derived numbers, decoded intents — still reach the application?
That question has a numerical answer, and the place to get it is the receiving end. AxonOS now has a program whose only job is to produce that number and to fail loudly if it is not zero.
git clone https://github.com/AxonOS-org/axonos-stack
cd axonos-stack
cargo run --locked --release --bin reference_bci
The last line of the output is RESULT: VERIFIED, and the process exit code is the same verdict: 0 for verified, 1 for failed, 2 for a bad argument. Nothing has to be read or interpreted for CI, a reviewer or a sceptic to know what happened.
It needs no hardware, no account, no network after the dependencies have been fetched, and no trust in us. The same seed produces the same output, byte for byte, on any machine. The output ends with SHA-256 digests of the run, and the trace digest is published below so you can check that your run is ours.
AxonOS is built as separate crates, each with one job and its own tests: acquisition, signal conditioning, a privacy boundary, a supervisor that decides whether the system may act, a consent state machine and an application SDK. Each is tested against the layer below it.
That arrangement has a blind spot, and axonos-stack was created to cover it. As its README has said from the first release:
Each organ is tested against the one below it. Nothing tested the chain — and a chain is where the interesting failures live, because every component is correct about its own contract and wrong about its neighbour's assumptions.
axonos-stack already holds the reference session: four crates wired together, run deterministically from a seed, with a transcript that CI compares byte for byte. It has caught real defects before. One was a grant larger than the audit log could record. Another was a refusal the session silently swallowed. A third was a timing table that had never been measured.
The reference BCI is the second binary in that repository, and deliberately not a new repository. A new repository would have been the easy way to make an integration problem look like a product. Putting it next to the session means it shares the lockfile, the pinned dependency tags, the tag-tampering check and the CI that already guards the session.
Release 0.4.0 adds it. The existing session transcript is unchanged.
SimDevice ─→ pipeline ─→ supervisor ─┬─→ vault ──── grant 1 ─────────→ DERIVED ─┐
axonos-hal re-reference posture │ sealed RAW, bits budget ├─→ application
screen └─→ decoder ─── consent gate ──→ INTENT ───┘ axonos-sdk types
band power axonos-consent
Six AxonOS crates take part, every one pinned to a release tag:
| Crate | What the run uses |
|---|---|
axonos-hal v0.3.0 |
SimDevice , a deterministic converter with scheduled faults; the 250 SPS timing budget |
axonos-signal-pipeline v0.9.2 |
common-average re-reference, the artifact screen, Goertzel power at 8, 10, 12 and 15 Hz |
axonos-supervisor v0.1.3 |
posture: whether a window may be classified, and whether its signal can be trusted |
axonos-vault v0.2.2 |
the sealed window of raw frames, a contact-quality reduction, grant 1, revocation |
axonos-consent v0.9.2 |
the consent state machine, the publication gate, strict Ed25519 verification |
axonos-sdk v0.3.5 |
the application's manifest, IntentObservation , and the error the application receives |
Nothing in the run is new machinery. The permission model is axonos-consent's, the disclosure model is axonos-vault's and the application types are axonos-sdk's. The new parts are the wiring that connects them and a verifier that checks the wiring. As it turned out, the wiring was the part that mattered.
The run keeps three kinds of data apart, because they carry different risks and need different rules.
RAW neural data means the sample frames themselves: eight channels of 24-bit codes, a sequence number, a timestamp and the contact state of each electrode. Raw frames are admitted to the vault's sealed window and go nowhere else. The application has no raw channel at all. That is a property of the wiring rather than something counted, and the output says so instead of printing a reassuring zero as though it had been measured.
DERIVED data is a reduction computed inside the boundary. Here it is a contact-quality reading: how many frames in the last second had an electrode off contact. It leaves only through Vault::release under a grant, and the grant has a budget measured in bits. Every disclosure is charged against that budget and written to the vault's audit log. The rules come from RFC-0009: a grant whose budget the log could not record is refused when it is issued, and a request that probes the vault's state is charged as if it were a disclosure. Bits are finite, so information leaving the boundary is finite as well.
APPLICATION INTENT is what the application actually consumes: an IntentObservation from axonos-sdk. That is a 32-byte record holding a timestamp, a kind (direction, workload or signal quality) and a fixed-point confidence. Intents leave only through axonos-consent's PublicationGate.
The gate is worth a paragraph, because its design is what makes "zero" more than an assertion. It keeps the consent state and the count of published observations in one 32-bit atomic word. Two bits hold the state and thirty hold the count. A publication commits with a single compare-and-swap that succeeds only while the state bits read Granted. A withdrawal rewrites the state bits of the same word. Both operations act on one atomic object, so they are totally ordered. Once a withdrawal has been stored, no publication can succeed, and none that commits afterwards can ever become visible. axonos-consent checks that ordering under every interleaving the C11 memory model allows, using loom.
The default run is 12,000 frames at 250 samples per second, which is 48 seconds of simulated session. It uses the simulator's FIELD fault profile: an overrun every 500 frames, a corrupt frame every 997, and an electrode that lifts at frame 1,200 and never comes back. The transcript reports only the events that matter for the boundary. Everything else goes into the trace digest.
t= 0.000s CONSENT Granted — fresh installation, manifest 1
t= 2.000s POSTURE Nominal → Degraded (FrameLoss { lost: 2 })
t= 2.512s POSTURE Degraded → Nominal (Recovered)
t= 4.000s POSTURE Nominal → Degraded (FrameLoss { lost: 2 })
t= 4.512s POSTURE Degraded → Nominal (Recovered)
t= 4.828s POSTURE Nominal → Degraded (LeadOff { frames: 8 })
t= 4.924s POSTURE Degraded → Restricted (LeadOff { frames: 32 })
The first five seconds belong to the supervisor. Two frames are lost to an overrun, so posture degrades and recovers half a second later. Then the electrode lifts at 4.8 seconds. Posture degrades 28 milliseconds later, and 96 milliseconds after that it is Restricted. From then on the system keeps recording, may not act, and treats every window as untrustworthy. That behaviour comes from the existing stack, and the reference BCI inherits it unchanged.
t= 18.000s CONSENT truncated frame (95 bytes) refused: WireFormatLength · state Granted
t= 18.000s CONSENT withdrawal signed by an unknown key refused: SignatureInvalid · state Granted
At eighteen seconds, two hostile frames reach the consent machine. One is a valid frame with its last byte cut off. The other is a well-formed withdrawal signed by a key the machine has never trusted. Both are refused for the reason the specification gives, and the state stays Granted.
Refusing a forged withdrawal can look backwards, since stopping is the safe direction. It is still correct, because the trusted path has to be the only voice in both directions. A machine that accepts unauthenticated frames when they say "stop" has already given up the property that protects the next frame, the one that says "start". axonos-consent admits a frame only after it parses as exactly 96 bytes with the right magic, verifies under strict Ed25519, carries a sequence number higher than any already consumed, and requests a transition that is allowed from the current state. Each of those layers has a Kani harness in that crate.
t= 36.000s CONSENT signed withdrawal, sequence 1 admitted → Withdrawn · state Withdrawn
t= 36.000s VAULT grant 1 revoked
t= 36.000s GATE intent suppressed (Withdrawn); application receives ConsentWithdrawn, terminal
t= 36.000s VAULT derived release refused: Revoked
At frame 9,000 the trusted path sends a signed withdrawal. The machine admits it and, before handle returns, the gate's state word reads Withdrawn. In the same step the session revokes the vault grant. Frame 9,000 happens to be a decision frame, so the decoder produces an intent there and then. The gate refuses it, and the application receives ConsentWithdrawn, an error the SDK classifies as terminal: the application must tear down its subscription. Frame 9,000 is also a release frame, so a quality reading is computed and the vault refuses it with Revoked. A withdrawal that lands on a frame governs that frame itself.
t= 36.004s CONSENT the same withdrawal again refused: Replay · state Withdrawn
t= 36.008s CONSENT signed re-grant, sequence 2 refused: InadmissibleTransition · state Withdrawn
Two more frames follow. The withdrawal is replayed byte for byte and refused as a replay, because its sequence number has already been consumed. Then comes a frame correctly signed by the trusted key that asks to return to Granted. It authenticates, it carries a fresh sequence number, and it is still refused: Withdrawn is absorbing. For this installation, observations never flow again. Resuming would require a new installation, and that is a decision for a person, not something one signed frame can do.
Then the summary:
DERIVED DATA · contact quality, released under grant 1
released 35 1120 bits
refused: grant revoked 12
refused: budget exhausted 0
received at/after withdrawal 0
APPLICATION INTENT · axonos-sdk IntentObservation
decisions 94 held by posture 0
generated 94 direction 1 · neutral 3 · quality 90
delivered 70
suppressed by consent gate 24
received at/after withdrawal 0
PERMISSION · axonos-consent
initial Granted
withdrawal at frame 9000 t=36.000 s
consent frames admitted 1
consent frames refused 4 0 answered otherwise than specified
final Withdrawn
post-withdrawal leakage 0
The verifier stands where the application stands. Every item the application receives, whether an intent or a derived disclosure, is tagged with the frame on which it was delivered. Anything delivered at or after the withdrawal frame counts as leakage. The run also checks four things that would expose a verifier being fooled:
- The gate agrees with the application. The gate's own publication count must equal the number of intents the application received, so nothing reached the application without passing through the gate and nothing passed the gate without arriving.
- The vault agrees with the application. The vault's total of released bits must equal the bits the application received. The same reasoning applies to the other channel.
- The accounting identity holds. Frames delivered plus frames lost must equal frames produced, and the supervisor's independent count must agree. In this run that is 11,965 + 46 = 12,011. Twelve further reads failed their integrity check and were never admitted anywhere.
- The replay matches. The whole session runs a second time from the same seed. Four things must match between the two runs: the digest of the raw input, the digest of everything the application received, the digest of the full event trace and the event log itself.
CHECKS
✓ accounting 11965 delivered + 46 lost = 12011 produced, supervisor agrees
✓ gate count = intents received 70 = 70
✓ vault bits = bits received 1120 = 1120
✓ post-withdrawal leakage = 0
✓ every consent frame answered as the specification requires
✓ deterministic replay 2 runs, 0 mismatch(es)
The principle generalises beyond AxonOS. You verify a boundary where the data arrives, not where the policy is written. A consent flag is a claim about intent. A count at the receiver is a measurement of effect.
A verifier that has only ever printed zero has not yet shown that it can print anything else. So the reference BCI carries two deliberate defects, reachable only from its tests and never from the command line:
- Bypass the gate. Intents are published without asking the consent gate.
- Skip the revocation. The withdrawal is admitted, but the vault grant stays live.
Each defect must make the run fail, and each has a test that asserts it does: leakage above zero, verified() false. If someone later "simplifies" the verifier into something that cannot see a leak, those two tests break first.
This is mutation testing in miniature, and it costs about forty lines. The second mutation is not hypothetical. It is the defect this work found.
axonos-consent is correct. Its gate stops intents the instant a withdrawal is admitted, and it has the proofs and concurrency models to back that up.
axonos-vault is correct too. A revoked grant refuses every later release, reports Revoked rather than a misleading BudgetExhausted, and its tests show it.
Nothing connected them. Nowhere in the AxonOS code, not in the kernel, the SDK or the stack, did anything revoke a vault grant when consent was withdrawn. The AxonOS Standard says a withdrawal must terminate every open observation stream for the manifest within ten milliseconds. It does not say that a vault grant is one of those streams, and no code treated it as one. The consent crate stops intents. The vault stops disclosures only when it is told to. Nobody told it.
The consequence is concrete. Wire the two in the obvious way, with consent in front of intents and the vault in front of disclosures, and a withdrawal stops the intents while derived data keeps flowing under a grant that is still live. In the default run that means twelve contact-quality releases, 384 bits, delivered after the person said stop. The skip-revocation mutation reproduces exactly that, and it is why the headline says the count would have been twelve.
This was not a bug inside either crate, and neither crate's tests could have found it, because each was right about its own contract. It was a gap between them, and the only thing that could find it was a program that runs both and counts at the receiver. That is the thesis axonos-stack was founded on, now confirmed for a second time.
The fix is three lines: when the consent machine admits Withdrawn, revoke the grant in the same step, before any further release. The unfixed version stays in the repository as a test that has to fail.
There is a second lesson here, and it is the one that should outlast this release. The fix currently lives in the reference session, which makes it the integrator's job. It belongs in the kernel's consent service, so that no integrator can forget it. That move is the first item on the list at the end.
When the gate refuses a publication, the kernel has to tell the application why. axonos-consent documents the bytes it hands across that boundary as the error the SDK delivers: 0x05 for suspended and 0x06 for withdrawn. axonos-sdk gives the same two errors the codes 0x0301 and 0x0302.
Each crate is internally consistent, and the two disagree. A kernel that forwarded the consent crate's byte to an application linked against the SDK would deliver a number the application does not recognise. The reference BCI maps the refusal by variant, not by code, so the right error arrives either way.
The two numbering schemes are recorded and not reconciled. Choosing the canonical one is a specification decision: it touches the AxonOS Standard's error section and any conformance vector that encodes those codes. A specification decision belongs in an RFC, not in a demonstration commit.
A repository that publishes its own defects should start with its own README. In this one, the table of pinned components listed older versions than the ones the build actually used, and the heading still said three components after the stack had grown to four. The citation file named the previous version, and a compiled Python cache file had been committed by accident. All of that is fixed in 0.4.0, and the changelog records it under Fixed instead of letting it disappear in a diff.
Look again at the intent line from the default run:
generated 94 direction 1 · neutral 3 · quality 90
Ninety-four decisions produced a single direction. Some would call that a weak demonstration. We think it is the correct one.
Under the FIELD profile an electrode lifts at 4.8 seconds and never returns. From then on the supervisor marks every window untrustworthy, and the decoder does what a responsible decoder should: it does not turn an untrusted window into a direction. It produces the observation the SDK provides for exactly this case, a signal-quality report saying Quality::Low. The application learns that the signal is bad, which is true, rather than learning "left", which would have been a guess presented as a decision.
The CLEAN profile shows the same chain on a device that behaves perfectly:
generated 94 direction 56 · neutral 38 · quality 0
Both transcripts are committed, and CI compares both byte for byte on every push.
The decision rule is fixed and published rather than tuned. Narrowband power is measured at 8, 10, 12 and 15 Hz, an SSVEP-style layout mapped to up, right, down and left. The strongest band becomes a direction only if it holds at least half the power of the four. Otherwise the decision is Neutral. Decisions run at 2 Hz, because the application declares the SessionQuality capability, the SDK's kernel limit for that capability is 2 Hz, and the SDK refuses a manifest that asks for more. The binary checks that limit at compile time as well as at run time.
One sentence matters more than all the rest of this section: the input is white noise. SimDevice produces deterministic, plausible-amplitude noise, not EEG. The directions mean nothing, and decoding accuracy is not what is being claimed. What is being shown is where intents may and may not flow.
Every claim in AxonOS carries an evidence level. The reference BCI uses three, and marks a level only where evidence exists.
| Level | In this release |
|---|---|
| L1 — derived / formally verified | None produced by this run. The relevant L1 evidence lives in axonos-consent : Kani harnesses for canonical decoding, authentication, replay refusal and the absorbing withdrawal, andloom models for the gate. |
| L2 — runtime measured | Behaviour only: counts and SHA-256 digests of a deterministic session on a host, from synthetic input. No timing is measured. |
| L3 — independently instrumented hardware | None. |
Each binary prints a timing line, and it deserves care. It says 972 µs. That figure is the end-to-end response time axonos-hal admits configurations against. RFC-0001 states it as L2: measured on an STM32F407 reference platform over a twelve-hour soak of about 10.8 million epochs, with no deadline misses. The soak trace itself is listed in the AxonOS claims register as publication pending. So the reference BCI says what it can stand behind: not measured by this run.
Four more limits, stated plainly:
- Real EEG. Covered above. The input is synthetic and the transcript header says
synthetic inputon its second line. - Attestation. In the SDK's design the kernel attaches a truncated HMAC-SHA256 to every intent. This session does not run a kernel, so the tag is zero rather than an imitation of one.
- Concurrency. The session is single-threaded. Withdrawal and revocation happen in one step, with nothing able to run between them. How the gate behaves under concurrent publication is established by
axonos-consent'sloommodels, not by this run. - Erasure. The sealed window of raw frames is not destroyed on withdrawal. The consent specification does not require it, and a demonstration is no place to invent a rule.
The list is long on purpose. When a demonstration lists what it does not show, you can trust what it does show.
Every event in the session is folded into one SHA-256. Each one is encoded as a frame number, a one-byte tag, a two-byte length and a payload. The payloads cover raw frames, acquisition errors, artifact findings, posture transitions, every consent frame together with its result, the revocation, every derived release or refusal, and every intent generated, published or suppressed. Next to that digest sit two narrower ones: one over the raw input alone, and one over exactly what the application received, in order. The trace format is documented in the source, table and all.
For the published commit, seed 7 gives these digests:
FIELD trace SHA-256 cfaa12273ba1de9093cf9ea6a044d7fc9b47480cf0f893a69d5b623265a20629
CLEAN trace SHA-256 984124f0efeb8b0479843768455d39c1ebe24c34762e8696d5abe90227a78479
If the same commit prints a different digest on your machine, that is a bug report we want to read. Determinism is a claim like any other, and it should be falsifiable.
Other runs worth trying:
cargo run --locked --release --bin reference_bci -- --json
cargo run --locked --release --bin reference_bci -- --profile clean
cargo run --locked --release --bin reference_bci -- --withdraw-at 0
cargo run --locked --release --bin reference_bci -- --withdraw-at never
cargo run --locked --release --bin reference_bci -- --frames 100000 --withdraw-at 60000
The long run is the most instructive. The grant's 2,048 bits are used up after 64 readings, so derived releases are refused as BudgetExhausted long before the withdrawal. After the withdrawal they are refused as Revoked. The vault checks revocation before budget on purpose, so its audit trail records the true reason rather than whichever check happened to run first. Leakage is still zero.
The test suite (cargo test --locked, 24 tests) covers:
- consent granted, consent withdrawn before the first frame, and consent withdrawn mid-session;
- every malformed frame shape;
- replay, hash reproducibility, and hashes that change with the seed and with the withdrawal frame;
- both transcripts, the JSON report, the 100,000-frame session and argument checking;
- the two deliberate defects.
If you build BCI software, the pattern carries over to any stack: count at the receiver, keep the broken version as a test, and hash the trace. None of that depends on AxonOS. It depends on deciding that "honoured" means a number.
If you evaluate, certify or regulate, this is a claim you can re-run in a minute and diff against a published transcript. It also tells you, in the same output, which parts are proven, which are measured and which are not shown. A rule about neural data that can be checked by recomputation is a different kind of rule from one that can only be checked by trust.
If you invest in neurotechnology, there may be no cheaper due-diligence question than this one: show me the count after withdrawal. A team that can answer it with a command has thought about the boundary. A team that answers it with a paragraph has not got there yet.
The release closes one question and opens five, in roughly this order:
- Move the revocation into the kernel. The withdraw-then-revoke wiring belongs in the kernel's consent service, so that honouring a withdrawal on both channels is not left to each integrator to remember.
- Reconcile the codes. One numbering for the suppression errors across
axonos-consent,axonos-sdkand the Standard, decided in an RFC, with the conformance vectors regenerated to match. - Publish the soak trace behind 972 µs. Once it is public, every binary that prints the figure can point to it. Until then, the binaries keep saying the figure was not measured by them.
- Go to L3. Run this chain on an ADS1299 front end and an STM32F407. Toggle GPIO lines at converter data-ready, at withdrawal admission and at the last publication, and capture them on an instrument with its own clock. Then measure, on hardware, the one latency this article has carefully avoided claiming: from the moment a withdrawal is admitted to the last observation that could ever be published. The Standard bounds it at ten milliseconds, and nothing has yet measured it against that bound. Publish the raw captures with the result.
- Real signals. Replay a public EEG dataset through the converter's test-signal path, as RFC-0001 describes for the soak, so the intents carry meaning and the decoding stage can be judged on its merits rather than excused.
Each item will land the way this one did: as a commit, a transcript and a number anyone can recompute.
Privacy for neural data is usually argued in sentences: policies, principles, promises. Those matter, and laws are now being written around them. But a sentence cannot be re-run.
This release argues one narrow point in a different form. A person withdraws consent at second thirty-six. From that frame on, the application receives nothing: no raw samples, no derived readings, no decoded intents. The program that says so checks itself against two versions of itself that leak, replays the whole session to make sure it agrees with itself, and prints a digest you can compare with ours.
The number is zero. Building the program that counts it is how we learned it would otherwise have been twelve.
Links
- Repository and release: github.com/AxonOS-org/axonos-stack ·v0.4.0
- Transcripts:
reference/reference-bci-7.txt·reference/reference-bci-7-clean.txt - Components: axonos-consent ·axonos-vault ·axonos-sdk ·axonos-hal ·axonos-signal-pipeline ·axonos-supervisor
- Design record: axonos-rfcs ·axonos-standard
© 2026 Denis Yermakou — The AxonOS Project · axonos.org · connect@axonos.org · security@axonos.org