openaliro/architecture
openaliro/openaliro
openaliro
Architecture
Every subsystem on one page, in reading order: entry points (nothing imports them) first, then the machinery they drive. Each heading links to the full per-module reference.
Fly through the whole surface
Every file the docs cover, in one 3D graph — orbit it, filter it, click a node for what it does.
Open the 3D viewflowchart LR
flowchart LR discord-boot --> participants chunk-kconfig --> corpus chunk-kconfig --> golden corpus --> golden deepwiki --> golden headers --> corpus headers --> expand independent --> corpus independent --> expand independent --> golden independent --> headers independent --> retrieve retrieve --> corpus retrieve --> golden scope --> corpus scope --> golden scope --> retrieve score-deepwiki --> golden stage0 --> corpus stage0 --> golden stage0 --> retrieve stage1-probe --> chunk-kconfig stage1-probe --> corpus stage1-probe --> expand stage1-probe --> golden stage1-probe --> headers stage1-probe --> retrieve build-size-baseline --> size-baseline-extract build-spec-index --> spec-scan check-citations --> drift check-citations --> citations drift --> citations register-commands --> index register-role-metadata --> roleConnection size-baseline-extract --> size-baseline api --> env assets --> assets.generated citations --> signatures command --> discord command --> env build --> api build --> build-targets build --> command build --> db build --> discord build --> followup context --> boards context --> command context --> discord context --> followup context --> rigs decode-devid --> citations decode-devid --> command decode-devid --> discord forget --> boards forget --> command forget --> discord forget --> followup forget --> rigs help-me --> api
Modules
activityactivity8 modules
discord-boot.tsDiscord Activity boot shim for the openaliro web twin. The twin is a standalone page that knows nothing about Discord and must keep working when opened straight off disk. So this file is the entire Discord surface: it detects the embedded context, marks the document so CSS can adapt, and completes the SDK handshake. It does not touch the simulation, it does not request an OAuth scope, and it holds no secret -- the client id is public by design and is injected at build time. Anything beyondready()belongs in a later phase.participants.ts"N watching": free social presence, no backend. Discord synchronises no state between Activity instances, so each viewer drives their own twin. This strip is the one honest exception: it shows who else has the Activity open, which makes a shared session feel shared without a sync server behind it. Everything here is untrusted input. Discord's own documentation says not to treat what the SDK reports client-side as truth, and a username is a string another person chose. Nothing in this file ever reaches innerHTML.boot-probe.pyPhase 1 check: the boot shim must never cost the twin its self-test.iframe-checks.pyTwo Activity checklist items that only mean anything inside an iframe.participants-test.mjsHostile-input test for the "N watching" strip. Usernames are strings other people chose, and Discord's docs say not to trust what the SDK reports client-side. This drives src/participants.ts with names designed to break out of the strip, in a real browser DOM, and asserts that none of them do. Run: node scripts/participants-test.mjs The strip is bundled with esbuild and handed a duck-typed SDK, so the code under test is the real module rather than a copy of its logic.verify-deploy.mjsCheck that what a host actually serves is what we built. A CDN is entitled to compress, cache and rewrite. twin.js is a binary file wearing a .js extension, so a host that "helpfully" minified or re-encoded it would corrupt the firmware while still returning 200 and looking fine in a browser tab. This fetches the deployed files and compares them byte for byte against the local build, and reports the response headers so an injected CSP cannot arrive unnoticed. Usage: node scripts/verify-deploy.mjs https://your-host.examplewrite-lock.mjsRegenerate activity/twin.lock.json from the current web-twin/ sources. Run this only when the twin was rebuilt on purpose (make twin-wasm). The lock exists so that a changed firmware blob has to pass through a reviewed commit instead of riding along in a deploy, so refreshing it without looking at what changed defeats the point.vite.config.ts
botbot87 modules
index.ts@file The interactions endpoint. An HTTP interactions Worker, not a gateway bot: no persistent socket, no privileged intents, no process with an uptime obligation. Discord POSTs here, this file answers within the 3 second deadline or defers. The order below is load-bearing and is checked by index.test.ts: 1. reject anything that is not a POST 2. read the body as TEXT, never as JSON 3. verify Ed25519 over timestamp + that exact text 4. only then parse Verifying after parsing would still reject bad signatures, but it would run a JSON parser on unauthenticated input first, and it would answer Discord's deliberately invalid PING with a 400 instead of a 401. Discord refuses the URL for the second one.register-commands.ts@file Upload the command list to Discord. Guild-scoped by default because guild registration is instant and global registration lags, which during setup reads as "the bot is broken". Bulk overwrite (PUT), not create: the array in src/commands/index.ts is the whole truth, so a command deleted from the code disappears from Discord on the next run instead of lingering as a slash command with no handler. DISCORD_APPLICATION_ID=... DISCORD_BOT_TOKEN=... DISCORD_GUILD_ID=... \ npm run register The token is read from the environment, never from a file in the repo, and is never printed, including in error paths.stage1-probe.ts@file Which candidate fix actually moves the config stratum? Four variants over the same golden set, so the Stage 1 build order is chosen by measurement rather than by the order the design brief listed them in. naive the committed baseline: 40-line windows kconfig grammar-aware chunks for .conf files expand naive chunks, query expanded through a vocabulary alias table kconfig+expand both headers naive chunks, each given a deterministic keyword header The alias table and the overfitting hold-out live inexpand.ts, because the independent-set scorer has to run the same expansion.independent.ts@file Validate and score a golden set this session did not write. Every number in this directory rests on 183 questions written by the same agent that then graded them, which is the one weakness the harness cannot measure about itself. A question written by somebody who has just read the answering line tends to share vocabulary with it, and lexical retrieval is exactly the technique that reward biases like that. So the headline finding — identifier phrasing retrieves at 0.93, prose phrasing at 0.38 — could in principle be an artifact of how the questions were phrased rather than a fact about the repository.independent.jsonlholds 90 questions written by three separate agents, each scoped to one slice of the tree and each told to draft its questions from an imagined situation BEFORE opening any file that might answer them, so the wording could not be copied off the line being cited. None of them readbot/eval/. This script then: 1. rejects every anchor that does not resolve, so a hallucinated path or a misquoted substring cannot enter the measurement, 2. classifies each question mechanically as identifier-phrased or prose-phrased, by whether it contains a token the retriever can match exactly, and 3. scores the same retrieval stack on both sets under the same classifier. Blindness here is by instruction, not by sandbox. It cannot be proved from inside, and the honest reason to believe it is the result: an agent that had read the answer key would not have produced a set the harness scores 0.34 lower. Usage: node eval/independent.ts [dir] no argument score the committed independent.jsonl a directory score every indep-*.jsonl in it, for vetting a fresh batch before merging it inregister-role-metadata.ts@file Registers this application's Linked Roles metadata schema (a separate, one-time-per-change registration from the slash commands in register-commands.ts — different endpoint, no guild involved, since role connection metadata is scoped to the application, not a guild). Run with DISCORD_APPLICATION_ID and DISCORD_BOT_TOKEN in the environment. Neither belongs in this repository.scope.tsstage0.ts@file Stage 0 baseline D: the lexical floor, measured. Recall@k here is a retrieval metric, so this runs with no API key, no network and no model call. That matters: the Stage 0 gate can be decided for $0, and only the baselines that are somebody else's hosted service cost anything. A gold anchor counts as retrieved when a returned chunk covers the line that actually contains itsexpectsubstring at HEAD. That is the same test as "the citation resolves to the right lines at a pinned SHA", which is the third weighted stratum, so citation correctness is not scored separately for this baseline — it is what recall already means here.build-size-baseline.ts@file Regenerate src/size-baseline.generated.ts from firmware/size-baseline.json. npm run size-baseline Importing the full baseline file directly was tried first and rejected: it carries a per-symbol breakdown for every recorded config (3,200+ lines) to answer a question that needs six numbers, and bundling all of it nearly tripled the Worker (62 KiB gzip -> 193 KiB) for data/sizenever reads. This extracts only what/sizeprints, the same way spec-index.ts extracts only citations out of docs/ instead of bundling the prose.npm run driftvia size-baseline.test.ts fails if this file falls out of sync.check-citations.ts@file The drift gate. npm run drift Fails when a line cited by src/citations.ts no longer says what the table claims it says, or when a cited file is not one of the paths that starts the bot workflow. Both are run by .github/workflows/bot.yml. Exit 0 clean, 1 on a finding.deepwiki.ts@file Baseline A: DeepWiki, scored against the same golden set. DeepWiki answers in prose rather than returning chunks, so recall@k has no meaning here and pretending otherwise would make the two baselines look comparable when they are not. Three things are measured instead: fact — the answer contains the goldexpecttext itself file — the answer names the file the fact lives in cite — the answer gives apath:linethat RESOLVES to the gold line at the pinned SHA, which is Amendment 2's third weighted criterionfactis strict and understates a prose answer that paraphrases a doc comment correctly, sofileis reported beside it rather than instead of it. For a config or identifier anchor, where the gold text is a literal token a correct answer has to print,factis the honest measure. Responses are cached so scoring can be re-run without re-querying a free service.score-deepwiki.ts@file Re-score the cached DeepWiki answers, with a metric that is fair to prose. The strict metric in deepwiki.ts asks whether the answer contains the goldexpecttext verbatim. For a config anchor that is nearly fair, but it scored this as a miss: expect: CONFIG_BT_MAX_CONN=1 answer: "...for the DWM3001CDK target, this value is set to1" which is a correct answer. Reporting 0.020 on the identifier stratum off the back of that would have been a measurement artifact presented as a finding. So two metrics are reported side by side: strict — the gold text appears verbatim. A floor. fair — forSYMBOL=VALUEanchors, the answer names SYMBOL and states VALUE within 120 characters of it. For prose anchors, at least 80% of the gold line's distinctive tokens appear.fairis the honest read of whether the answer carries the fact.strictstays because it is the one a machine could check without judgement, and the gap between them is itself informative.build-spec-index.ts@file Regenerate src/spec-index.generated.ts from the live docs/ tree. npm run spec-index Run this after editing anything in docs/ that cites the Aliro 1.0 specification.npm run drift(via spec-index.test.ts) fails the build if the committed file falls out of sync with a fresh scan, the same way citations.ts is checked against the lines it cites.index.ts@file The command table. Registration and dispatch read the same list, so a command cannot be registered with Discord without a handler behind it, or gain a handler nobody can invoke. The modal and component routes are derived from the modules too, rather than kept as their own hand-written maps here: a table in this file lets a command ship a handler that nothing routes to, and the two drift apart silently.discord.ts@file Wire constants and response builders for the Discord interactions protocol. Kept dependency-free sotest/can exercise it without a Worker runtime.env.ts@file Worker bindings. Every secret here is set withwrangler secret put NAME. None of them is ever committed, logged, or echoed into a response. See bot/README.md for the full list and how to set it. One Worker serves both halves of this bot — firmware triage and hardware compatibility tracking — so this is the union of what both need. Anything optional degrades the one feature that reads it rather than failing the Worker, which is what letsnpm testand a fresh clone run with nothing set.linkedRoles.ts@file Orchestrates the Linked Roles flow across the three plain browser routes (/linked-role,/discord-oauth-callback,/github-oauth-callback) thatsrc/index.tsregisters alongside — but structurally separate from — the signed interactions endpoint. These are ordinary redirects a browser follows, not Discord interactions: no Ed25519 signature is involved or expected, which is also why they live on their own routes rather than folded intoPOST /. Each exported function takes the incoming request's own URL and derives both OAuth redirect URIs from its origin, so no separate "base URL" secret is needed — the Worker always knows where it is being reached at. Metadata is only ever pushed once, right after both OAuth legs complete. There is no scheduled refresh: re-running/linked-roleis how a contributor updates their badge later (a periodic full refresh across every linked user is a natural follow-up, not built here, since it would mean spending every linked user's share of GitHub's search rate limit on every sweep tick whether or not anything about them changed).scheduled.ts@file The escalation sweep: "asleep candidates get pinged on a follow-up if nobody accepts within a configurable window." Runs off a Cron Trigger (wrangler.toml[triggers]) rather than any in-request timer, since a Worker has no way to schedule work minutes after a request has already finished — this is the one part of the bot that is not driven by a Discord interaction at all.verify.ts@file Ed25519 verification of Discord interaction requests. Nothing in this Worker may parse a request body before this file has accepted it. Discord enforces that from the outside: when an interactions URL is saved it POSTs a deliberately invalid PING, and refuses the URL if the endpoint answers instead of rejecting. So the ordering is not a preference, it is the thing that makes the endpoint installable at all. Fails closed everywhere. A malformed header, a wrong-length key, an unsupported curve and a genuinely bad signature all return false, and the caller turns every one of them into the same 401.chunk-kconfig.ts@file A grammar-aware chunker for Kconfig fragments, and the experiment that says whether it earns its place. The prediction being tested is that it will NOT fix the config stratum.firmware/prj.conf:226fails today because the question says "serial port" and the file says "console", "RTT" and "UART" — and the naive 40-line window already contains that comment. Attaching the comment more precisely does not add a word the file never uses. What it should improve is precision: one chunk per CONFIG symbol, carrying its own comment and its section header and nothing else, means a hit is the fact rather than a 40-line neighbourhood that happens to contain it. That shows up in MRR and recall@5 rather than recall@10. Writing the prediction down first so the measurement can contradict it.corpus.ts@file What gets indexed, and how it is cut up for the Stage 0 baseline. The chunker here is deliberately naive: fixed line windows with overlap, no grammar awareness at all. That is the point. Stage 0 measures the floor, so the custom Kconfig/devicetree/Makefile chunkers proposed for Stage 1 have a number to beat rather than an assertion to agree with. File selection isgit ls-filesminus binaries and minus anything generated, so a hit can never come from a file that would be rebuilt rather than edited.expand.ts@file The vocabulary alias table, and query expansion over it. Lives on its own because two experiments need it: the Stage 1 probe, which measured it as the one candidate fix that moves the config stratum, and the independent-set scorer, which has to run the same expansion against questions written by somebody who never saw this table. On overfitting: it would be trivial to read the miss list, write an alias per failing question and report a wonderful number that means nothing. Every entry below is written from the domain's own vocabulary -- a serial port IS a UART, a console IS where logs go -- and not one was added by looking at which questions failed. It is also small enough to read in one screen, which is the honest way to let somebody check that claim.golden.ts@file The golden set, and the gate that keeps its labels honest. Gold labels key on anexpectsubstring, never on a bare line number. That is not a style choice: the design brief this eval was written against citedmk/cdk.mk:261-262, and commit 3737673 grew that file by 71 lines and moved every one of those anchors a uniform +20. A golden set keyed on line numbers would have silently measured every baseline against the wrong lines. Same mechanism as scripts/check-citations.ts, one level stricter: anexpectthat matches more than one line in its file is reported too, because an ambiguous anchor makes recall look better than it is.headers.ts@file §7.2 tier 1: a deterministic keyword header on every chunk. This is query expansion pointed at the index instead of the question. A chunk holdingCONFIG_UART_CONSOLE=ngains the wordsuartandconsole, and through the alias tableserial,port,tty,vcom— so a question that says "serial port" can reach a line that never uses either word. Symbol names and the file path are the only sources: no prose is invented, nothing is written by hand per symbol, and there is nothing here that could have been tuned against a particular golden question. Measured as dominated on the self-written golden set and kept anyway, because the independent set later showed why that verdict was premature. Questions like "how much stack does the main thread get" fail there even thoughmain,stackandsizeare all insideCONFIG_MAIN_STACK_SIZE: those words are so common in this tree that idf buries the one chunk that matters. Repeating a symbol's own words in a header does not add vocabulary, but it does add term frequency exactly where the answer is, which is a different lever from the one the self-written set was able to test.retrieve.ts@file The two lexical retrievers Stage 0 measures, over one shared tokenizer. Both baselines take the same term list, so a difference between them is a difference in the index and not in how the question was read. The FTS5 index is built twice on purpose. SQLite's unicode61 tokenizer treats_as a separator, which shredsCONFIG_UART_CONSOLEinto three ordinary English words and makes the single most common query shape in this repo un-retrievable.tokenchars '_'fixes it. Measuring both quantifies how much of "lexical search works here" is really "lexical search configured for identifiers works here".roleConnection.ts@file Linked Roles metadata: the five fields the spec suggests (boards_owned,has_nfc,ios_major,validated_runs,merged_prs), their Discord registration schema, and computing four of the five purely from this bot's own D1 tables.merged_prsis deliberately not computed here — it needs the linked GitHub account (githubOAuth.ts) and a call to GitHub's own API, a different failure domain from "read our own D1".size-baseline-extract.ts@file The extraction logic, separate from the file I/O in build-size-baseline.ts so size-baseline.test.ts can drive it against a fresh read without importing a script that writes files as a side effect.drift.ts@file The drift gate's logic, with no filesystem in it. A stale triage table is worse than no triage table, because people trust it. These checks are what stop src/citations.ts from turning into folklore: if an edit to mk/cdk.mk moves the line a citation points at, CI fails here rather than the bot quietly citing the wrong line at somebody for a year. Kept free ofnode:fsso the tests can drive it with fixtures instead of rewriting the repository.citations.ts@file The triage table. Every answer this bot gives comes from here. A lookup table, not a model. Each entry is a plain reading, a next command, and afile:lineinto this repository. Nothing is written from memory: if a failure mode is not documented in the tree it does not get an entry, and the bot escalates instead of guessing. A confident wrong diagnosis costs somebody an evening, which is worse than no answer at all.expectis the drift guard. scripts/check-citations.ts reads each cited line and fails if the substring is no longer on it, so an edit to mk/cdk.mk that moves a line breaks CI rather than quietly turning this table into folklore. Keepexpectshort and distinctive, and never let it span a line break.spec-scan.tscommand.ts@file The shape every command file exports: a Discord command definition (for registration) and a handler (for dispatch), together so the two never drift apart. Separate from commands/index.ts so that followup.ts can take a context without importing the command table that imports it back.build.ts@file/build <target>— dispatch firmware-builds.yml. The heaviest thing this bot can ask CI for: up to six jobs, the NCS and ESP-IDF toolchains, tens of minutes. Deferred unconditionally, rate limited harder than anything else here, and idempotent on the interaction ID so a retried delivery cannot dispatch twice.context.ts@file/context— the firmware-sidemake doctorthe tree does not have. The HA agent has adoctorstep (docs/home-assistant.md:120); nothing equivalent exists for the firmware itself. This is not that tool, and it does not pretend to be: it cannot inspect a contributor's machine from a Cloudflare Worker. What it can do is print the one fact this repository pins (the NCS version) next to what a report is missing, as one block ready to paste, so asking for it is not a fourth round trip.decode-devid.ts@file/decode-devid— the most common bring-up failure.make selftestlogs the raw DEV_ID read from the DW3110 over SPI. The tree documents exactly two outcomes for that value, and this command encodes those two. Anything else is reported as unrecognised: a DEV_ID that is neither the healthy prefix nor one of the two dead reads is a fact nobody here has written down, and inventing a reading for it is how a triage bot starts costing people evenings.forget.ts@file/forget— hard delete. Not a flag, not a soft delete, not a tombstone. The row goes. With noboardargument, every row for the invoker goes.help-me.ts@file/help-me— collect the context once, match it, escalate honestly. The whole point of this bot. Support here costs three or four round trips before anyone knows which board, which image, and what the console said, so this asks for all of it in one form and posts a thread that already has the answer or already says there isn't one. Two interactions, not one. Discord will not let a command defer and then open a modal, so board and image are command options (enumerated, validated by Discord) and the free text is collected by the modal that the command returns. The selections ride through on the modal's custom_id. The matcher never speculates. No match is reported as no match and pings the maintainer, because "I don't recognise this" is a useful thing to tell somebody and a plausible guess is not.ihave.ts@file/ihave— register hardware. Board, radio and NFC front-end are command options: Discord fills these in fromchoicesbefore the interaction ever reaches this Worker, which is a client-validated dropdown exactly like a modal string select would be, and costs nothing against a modal's (unconfirmed) component limit. See bot/README.md for why those three fields live here and not in the modal. Everything else — phone model, iOS version, the awake window, and the UTC offset — is free text or needs a 25-option select the command-optionchoiceslist cannot hold (iOS version) or is genuinely one-shot text (the awake window), so it goes in a modal. The three option values are not available on the follow-up MODAL_SUBMIT interaction — it is a separate interaction — so they are smuggled through the modal's owncustom_id.matrix.ts@file/matrix— the compatibility matrix. Public and non-ephemeral: this is the artifact people screenshot, and a screenshot of a message only its poster can see is not useful to anyone else. Tries the PNG render first; falls back to the monospace table from bot/README's "phase 3" both when the per-user cooldown is still active (PNG rendering is the expensive path) and when rendering itself throws, so a WASM or layout failure degrades the command instead of erroring it.ping.ts@file/ping— the liveness check. Answers only if the signature already verified, so a successful reply proves three things at once: the endpoint is reachable, the public key binding is the right one, and the Worker is inside the 3 second response deadline without deferring. That is the whole point of it, and it is why this command touches no binding: it must not be able to fail for a second reason.size.ts@file/size— the current recorded CDK size baseline. Readsfirmware/size-baseline.jsondirectly (src/size-baseline.ts), the same filemake cdk-size-checkcompares a build against andmake cdk-size-baselinerewrites. This is a snapshot from the last commit that updated it, not a live measurement: nothing here can build firmware, so a stale answer is possible if the record has not been refreshed since a change moved the numbers. The commit the baseline was recorded at is always printed so a reader can judge that for themselves.spec.ts@file/spec <section>— which files in this repository cite an Aliro 1.0 section. Pointers only. The spec text itself (internal/aliro-1.0.txt) is gitignored and this bot never reads it;SPEC_CITATIONSis built by scanning the tracked prose that already cites the spec, not the spec. Answering with anything more than a file and a line would start reproducing member-confidential text one paraphrase at a time, which is exactly what this command must not do.test-request.ts@file/test-request board: ios: what:— maintainer-only. Looks owners up the same way/who-hasdoes, partitions them into awake and asleep from their stored UTC offset and awake window, posts a status Container to the fixed test-queue channel (not wherever the command was run —TEST_QUEUE_CHANNEL_ID), and pings only the awake half. Nobody's identity appears in the persistent card itself, only aggregate counts, matching this bot's "user IDs are not a browsable roster" posture even though this command's whole job is finding and reaching specific people: the card is what everyone in the channel sees, the ping is a disposable message naming exactly the candidates being paged.test-result.ts@file/test-result pass|fail— run inside the claim thread, by whoever claimed it. Closes the job (claimed -> done, atomically, so a retried or duplicate delivery cannot record two validations for one claim), writes a validation row, and edits the original queue Container to a pass/fail accent — the one edit in this bot that a component's own interaction token cannot make, since this command runs in the thread, a different channel from the card it needs to update, so it goes through the bot token like the escalation sweep does.twin.ts@file/twin approachand/twin explain— the WASM digital twin, run inside this Worker./twin approachdrives the real compiled woz_uwb responder (twin.ts, web-twin/twin.js + twin.wasm) through a simulated walk-up and reports what the trust gate actually decided, with a file:line citation. It answers protocol/crypto-maths questions; it proves nothing about PDoA/AoA, NFC Express Mode, iOS point-release behaviour, or physical approach unlock — every reply says so./twin explain <hex>is NOT implemented. twin_glue.c exports no entry point that ingests a raw wire frame — every call in it synthesizes frames from a target distance (twin_mk_prepoll/twin_mk_final_data), and the whole exchange is CCM*-encrypted against a twin-internal test URSK a real board's traffic was never encrypted under. Decoding a pasted ranging block from a bug report would need a new C-side entry point in ccc_shim_rx.c/twin_glue.c — a firmware change, which this instrumentation task is not allowed to make (see the parent prompt's Hard Constraint 4). The subcommand is registered so/twin explainnames its own gap instead of 404ing, and says exactly that rather than attempting a decode that cannot work.verify.ts@file/verify <sha256>— attestation lookup by subject digest. Answers "did openaliro/openaliro's CI actually build this file", from GitHub's public attestations API, not from a SHA256SUMS.txt served next to the artifact it describes (a compromise that could replace the binary could replace that file in the same motion —scripts/security-attest.sh's own reasoning for why this control exists at all).who-has.ts@file/who-has— maintainer-only lookup. Returns user IDs to ping, not a browsable roster:default_member_permissions: "0"hides it from everyone without guild administrator rights, and the handler checks the invoker against MAINTAINER_IDS regardless, because the first is a server setting somebody can change and the second is not.why.ts@file/why— the recurring "is this a bug?" answers. Every one of these is a thing the tree already says, that a contributor has no reason to have read. Answers publicly rather than ephemerally: the point is that the next person sees it too.discordOAuth.ts@file The Discord half of Linked Roles: authorize URL, the two OAuth2 token-endpoint grants (authorization_code, refresh_token), identifying who authorized, and pushing the final role-connection metadata. Every endpoint and body shape here was checked against docs.discord.com (2026-08-04) rather than assumed — this is a different trust boundary from the rest of the bot (a real bearer credential granted by an actual user, not just their opaque Discord ID), so it is worth being sure.identifyis requested alongsiderole_connections.write: the metadata push endpoint is scoped to "whoever this access token belongs to", so the callback needsGET /users/@meto learn *which* Discord user just authorized before it can store anything against them.githubOAuth.ts@file The GitHub half of Linked Roles. It verifies the account, not the person, and never learns more than that. In practice that means this only ever asks for the account's public identity: no scope is requested in the authorize URL at all, sinceGET /userreturnsidandloginfor the authenticating account with no scope needed — anything broader would be more access than the feature uses. The resulting token is used once (by the caller, in the OAuth callback) and is never stored; onlyid/loginpersist. Endpoints verified against docs.github.com (2026-08-04).oauthLinks.ts@file Every statement this Worker runs againstoauth_links. Same rule as every other D1 module here: no SQL built by concatenation, every value a bound parameter. Token encryption itself lives in tokenCipher.ts — this file only ever handles already-encrypted blobs.oauthState.ts@file The CSRF/session-correlation state that chains the two OAuth legs ("Discord authorize" then "GitHub authorize") into one flow, and stops a forged callback from attaching a GitHub identity to the wrong Discord user. Every transition is a single guarded statement (same atomic first-writer-wins shape asclaim()in testRequests.ts), not a read-then-write, so a replayed or duplicated callback cannot advance a state twice.discordRest.ts@file The handful of Discord REST calls that need a bot token rather than an interaction token: posting to a fixed channel regardless of where a command was invoked, starting a thread, and editing a message outside any live interaction (the scheduled escalation sweep). Everything else in this bot goes through the interaction's own token — see followup.ts — because that requires no secret beyond what Discord itself hands the Worker per request. Every function here fails soft: log and return null/false rather than throw, so a REST hiccup degrades one step of a request-routing flow instead of losing the D1 state already committed around it.testRequestContainer.ts@file The/test-requeststatus card, and the one-time ping that goes with it — kept as two separate messages rather than one, so the ping (disposable, mentions-bearing, ordinarycontent) never has to be reconstructed when the card (persistent, Components V2, nocontentallowed at all per the platform table this bot's spec cites) is edited in place later. Component shapes below are per docs.discord.com/developers/components (checked 2026-08-04): a Section (type 9)'saccessoryis the documented way a Components V2 message carries a Button next to text, not an Action Row inside a Container — the docs show no Container -> Action Row nesting at all, so that path was not used here. This has not been proven against a live Discord render, the same caveat as this bot's modal wire format.testRequests.ts@file Every statement this Worker runs againsttest_requestsandtest_request_candidates. Same rule as rigs.ts: no SQL built by concatenation, every value a bound parameter.rigs.ts@file Every statement this Worker runs against D1. Nothing outside this file writes SQL, and nothing in this file builds SQL by concatenation. Each query is a constant with bound parameters, so a value arriving from a Discord field cannot become syntax.validations.ts@file Every statement this Worker runs againstvalidations. Same rule as rigs.ts and testRequests.ts: no SQL built by concatenation, every value a bound parameter.size-baseline.ts@file The recorded CDK size baseline, as six numbers. Not a live import offirmware/size-baseline.json: that file carries a per-symbol breakdown for every recorded config (3,200+ lines) to answer a question/sizeneeds six numbers for, and bundling it whole nearly tripled the Worker (seescripts/build-size-baseline.ts). This reads the generated extract instead, checked against a fresh read of the real file bytest/size-baseline.test.ts.primaryis the configcdk-size.yml's own header identifies as the shipping image: SMP=1 RELEASE=1 with LTO, the onemake releasebuilds andmake fotapushes.signatures.ts@file The console-output matcher. A lookup table, not a model. Every entry below is a literal string that this repository already documents, with the line that documents it. Nothing here was written from what a log "probably" means: if the tree does not say it, there is no entry, and the bot escalates instead. Patterns are deliberately literal and distinctive. A loose pattern that matches half the pastes in a channel is worse than no pattern at all, because it produces a confident answer that happens to be wrong, and the person believes it for an evening before going back to the start. Ranking is by the length of the text that matched, longest first, so a specific error string outranks a short token that happened to appear. Ties keep declaration order. All matches are shown, never just the best one:URSK_Unavailablelegitimately has two documented causes and choosing between them is the reader's job, not this table's.api.ts@file The two Discord calls that need the bot token. Creating a forum post and posting into a thread are the only things this bot does that an interaction token cannot authorise. The token is a Worker secret, is never logged, and is never interpolated into anything that could be echoed back. Channel IDs are checked against a snowflake pattern before they reach a URL. They come from configuration rather than from a user, but a URL built by concatenation is worth validating whatever the source.build-targets.ts@file Thetargetschoicesfirmware-builds.ymlactually accepts. Hand-copied from.github/workflows/firmware-builds.yml'sworkflow_dispatchinput rather than parsed at request time, because a Worker has no access to that file.test/build-targets.test.tsre-parses the live workflow and asserts this list matches it exactly, so an added or renamed target fails the build instead of/buildsilently offering a stale choice.db.ts@file Interaction idempotency and the/buildcooldown. The hardware registry itself lives in rigs.ts, against therigstable. This file is what is left once that moved: the two tables that exist to stop a command running twice rather than to remember anything about hardware. Nothing outside this file and rigs.ts writes SQL, and neither builds SQL by concatenation. Each query is a constant with bound parameters, so a value arriving from a Discord field cannot become syntax.followup.ts@file Deferring, and the edit that finishes a deferred command. Discord gives an interaction 3 seconds. Anything touching D1 defers unconditionally rather than racing that clock, because a command that usually answers in 200 ms and occasionally does not is a command that occasionally fails for no reason a user can see. The interaction token authenticates the follow-up edit, so no bot token is involved here. It expires 15 minutes after the interaction.boards.ts@file The enums a contributor picks from: board, radio, NFC front-end, and UTC offset. Taken from the hardware axes in the spec this bot follows and from CLAUDE.md's own target table. Board, radio and nfc are enforced as Discord commandchoices, so a well-behaved client never sends anything outside the list. They are re-validated here anyway: choices are a client-side convenience, not a server-side guarantee, and every one of these values reaches a bound SQL parameter and a modalcustom_id.images.ts@file The images somebody can be running. The make targets and their build directories, from the dispatcher inMakefileand the per-target recipes inmk/. Bare targets mean the DWM3001CDK; the nRF5340 DK isnrf-prefixed and the ESP32 isesp-prefixed. "Not sure" is a real option on purpose. Somebody who does not know which image they flashed is exactly the person filing the report, and forcing a guess would put a wrong answer into the context block rather than a blank.modal.ts@file Modal building and parsing. Discord's current modal system wraps each field in a Label (type 18) component placed directly indata.components— no Action Row wrapper, which is the older, now-deprecated shape a text-only modal used. Verified against docs.discord.com/developers/interactions/message-components and .../components/reference on 2026-08-04: Label carrieslabelanddescription; the wrapped component (Text Input type 4, String Select type 3) carries no label of its own. A submitted modal comes back as a *flat* array of component values — not nested inside the Label — each withcustom_idand eithervalue(text input) orvalues(select). This is a newer, less-travelled part of the API than the rest of this bot's wire handling. It has not been proven against a live Discord round trip;test/modal.test.tsproves only that this file's own build/parse pair agrees with itself and with the shapes documented above.cooldown.ts@file The/matrixPNG-rendering cooldown. One statement, not a read then a write: SQLite serializes writes to one database, so the WHERE guard on the UPDATE is the atomicity. Two concurrent/matrixcalls from the same user landing in the same millisecond still only let one through, because there is no gap between reading the old timestamp and writing the new one for a second statement to land in.matrix.ts@file The compatibility matrix: pure formatting, no D1 and no Discord wire types, so it is testable without either. Only two of the four documented glyphs are reachable yet. ✅ (validated) and ❌ (known-broken) both come from test *results*, which is the/test-request+/test-resultmachinery — not built. Showing them here would be a status this bot has not actually observed, so for now every cell is either ⚠️ (someone owns that board/iOS pair) or ❓ (nobody does).matrixTabletakes an optional results map so this file does not need to change again once that phase lands.render.ts@file The compatibility matrix, as a PNG. Satori (JSX-shaped element tree + CSS-subset styles -> SVG) does the text shaping itself and bakes glyphs into vector paths, so the SVG it produces is self-contained — @resvg/resvg-wasm rasterizes it with no font of its own needed. WASM is initialised once per Worker isolate and reused, per the spec this bot follows ("Load WASM once at module init"). Colors are exactly the four semantic accents the spec defines for status Containers elsewhere in this bot (0x2ECC71/0xF1C40F/0xE74C3C/0x5865F2): reused here rather than inventing a fifth palette, so "owned" and "nobody owns it" read the same way in the image as they eventually will in a Container.spec-index.generated.ts@file Generated by scripts/build-spec-index.ts. Do not hand-edit. Regenerate withnpm run spec-indexafter editing anything in docs/ that cites the Aliro 1.0 specification. spec-index.test.ts fails if this file falls out of sync with a fresh scan of the tree.awake.ts@file Whether a registered owner is awake right now, from their storedutc_offset(minutes) and local awake window (awake_start/awake_end, hours 0-23). This is the routing logic the spec calls out by name: "a router that wakes people at 3 a.m. loses the contributors it was built to help", so it is built and tested against explicit timezone cases before anything wires it to a ping.maintainer.ts@file The maintainer allow-list, shared by every maintainer-only command (/who-has,/test-request). Adefault_member_permissions: "0"on the command definition hides it from non-administrators, but that is a guild setting anyone with server admin can change; this check is not.twin.ts@file Runs the compiled woz_uwb digital twin inside this Worker.web-twin/twin.jsembeds its WASM as a decoded byte string and instantiates it at runtime withWebAssembly.instantiate(bytes, imports)— the one path workerd refuses ("Wasm code generation disallowed by embedder", proven directly againstwrangler dev; see docs/twin-worker-phase0.md). twin.js itself is never edited: Emscripten'sModule["instantiateWasm"]hook lets this file hand it a build-time-precompiledWebAssembly.Moduleinstead (imported as a static.wasmmodule, the one WASM path workerd does allow), so twin.js's own embedded-bytes loader is never reached. twin.wasm is extracted once by scripts/twin-wasm-extract.ts and drift- checked against web-twin/twin.js by test/twin-wasm-drift.test.ts on every run — a rebuilt twin.js with no matching re-extraction fails that gate rather than silently running stale firmware. The.wasmimport below is deliberately dynamic, not a static top-level import: Node's own module loader treats a staticimport x from "*.wasm"as a native WASM-ES-module and tries to resolve twin.wasm's own imports (wasi_snapshot_preview1) as JS packages, which crashes immediately under plainnode --test(this repo's own test runner) — a real regression caught by running the full bot suite, not something wrangler's bundler does. A dynamicimport()is resolved lazily, only in the branch that actually calls it, so Node never touches the WASM path at all.attest.ts@file Look up a GitHub build-provenance attestation by subject digest.release.ymlrunsactions/attest-build-provenanceon every published asset (.github/workflows/release.yml,scripts/security-attest.sh), which binds an artifact's sha256 to the workflow, repository and commit that built it. This is the read half: "does this digest have one", answered from GitHub's own public attestations API rather than trusted from a SHA256SUMS file served next to the thing it describes. Read-only and needs no write scope. A token, if bound, only raises the unauthenticated rate limit; the lookup works without one for a public repo.tokenCipher.ts@file AES-256-GCM for the one thing this bot stores that is a real bearer credential rather than an opaque Discord user ID: the OAuth access and refresh tokens Linked Roles needs to push updated metadata later without re-prompting the user. D1 has no column-level encryption of its own, so this exists to keep those tokens unreadable from a raw table dump —OAUTH_ENCRYPTION_KEYis a Worker secret, never a D1 value.size-baseline.generated.ts@file Generated by scripts/build-size-baseline.ts. Do not hand-edit. The six numbers/sizeprints, extracted from firmware/size-baseline.json rather than bundling that whole file (see build-size-baseline.ts for why). Regenerate withnpm run size-baselineaftermake cdk-size-baselineupdates the source file. size-baseline.test.ts fails if this drifts from a fresh extraction.assets.ts@file Decodes the generated base64 assets into bytes, once per Worker isolate.atobis a Web platform global available in both Node's test runner and the Workers runtime, so this needs no environment branching.assets.generated.ts0001_rigs.sql0002_matrix_cooldowns.sql0003_test_requests.sql0004_validations.sql0005_oauth.sql0006_handled_interactions.sql0007_build_cooldowns.sqlgenerate-assets.ts@file Regenerates src/assets.generated.ts from the vendored font files and the installed @resvg/resvg-wasm's WASM binary. Both need to reach the Worker as bytes, and need to reachnode --testas the same bytes with no bundler involved — Wrangler can import.wasmand (with a module rule) arbitrary binary files natively, but Node's loader understands neither, and this bot's tests run the TypeScript sources directly. Base64-embedding sidesteps that split entirely: no import rule to keep in sync between environments, no bundler-vs-node --testskew. The cost is a large generated file (~1.7 MB gzipped, well under Cloudflare's 3 MB free-tier compressed Worker limit, confirmed against developers.cloudflare.com/workers/platform/limits on 2026-08-04). Run after upgrading @resvg/resvg-wasm or the vendored fonts: npm run generate-assetstwin-wasm-extract.ts@file Extract the WASM module embedded in web-twin/twin.js into src/twin.wasm./twinruns the compiled firmware inside a Cloudflare Worker, not a browser. workerd refuses runtime WASM code generation from a byte array (WebAssembly.instantiate(bytes, …)— the exact path twin.js's ownfindWasmBinary()/instantiateArrayBuffer()take — fails there with "Wasm code generation disallowed by embedder"; confirmed directly againstwrangler dev, see docs/twin-worker-phase0.md). A WASM module imported as a build-time module resource (import wasmModule from "./twin.wasm") is compiled ahead of time instead, which workerd does allow, and Emscripten'sModule["instantiateWasm"]hook lets a caller supply that precompiled module without twin.js ever reaching its own embedded-bytes path. This script produces that file — once, checked in — by requiring the real, unmodified twin.js under Node (where runtime WASM codegen is allowed) and capturing the exact bytes its ownWebAssembly.instantiatecall receives. twin.js is never edited, forked, or re-encoded; only observed. Run after any web-twin/twin.js rebuild:npm run twin-extractin bot/, then commit src/twin.wasm and src/twin.lock.json together.npm test's drift guard (test/twin-wasm-drift.test.ts) fails if twin.js changes without a matching re-extraction.twin-js.d.ts@file Ambient type for the relative import of ../../web-twin/twin.js. twin.js is a plain CommonJS file (Emscripten's default UMD output), not a TypeScript module, and lives outside src/ (it is consumed as-is, never copied — see twin.ts). This is only a type shape for the bundler's CJS interop; it says nothing about what twin.js actually does at runtime.wasm-module.d.ts@file Ambient type for a static.wasmmodule import. Wrangler's bundler compiles a.wasmimport into aWebAssembly.Moduleahead of time (the one path workerd allows — see twin.ts). TypeScript has no built-in type for that import shape.
firmwarefirmware/src15 modules
matter_commission.cjoins BTP, the exchange and PASE. Three finished pieces and no protocol of its own: matter_ble_zephyr.c bytes in and out over the 0xFFF6 service matter_exchange.c which session, which exchange, duplicate, ack matter_pase_sm.c the five commissioning messages What is left for this file is the wiring nobody else can do: pulling the SPAKE2+ verifier out of configuration, drawing real randomness, and deciding what happens when a commissioner disappears halfway through.aliro_ble_zephyr.cmain.cmatter_ble_zephyr.cthe 0xFFF6 GATT service that carries BTP. A thin adapter, on purpose. All the framing lives in modules/woz_matter (matter_btp.c), which has no Zephyr dependency and is tested on the host under sanitizers. This file does three things and no more: hand C1 writes to the reassembler, drive the fragmenter out through C2 indications, and build the commissionable advertisement. Modelled on aliro_ble_zephyr.c, which is the same shape -- proprietary service, one write characteristic, one indicate characteristic, connection-scoped state -- and is proven against live iPhones.matter_fab_settings.cmatter_ble_zephyr.hthe 0xFFF6 commissioning transport. Everything here is Zephyr-side glue. The protocol lives in modules/woz_matter, which knows nothing about BLE.matter_commission.hstart answering commissioning attempts.matter_fab_settings.haliro_prov_settings.ccase_bench.cdfu_ble_zephyr.cThe over-the-air update channel: a second L2CAP CoC, and the button that opens it. WHY NOT mcumgr. SMP over Bluetooth was built and measured first. It costs 3,717 B of RAM on an image that had 7,448 B left, and its permission model defaults to demanding a paired, authenticated link whenever BT_SMP is on -- which it is here, pulled in by L2CAP CoC. This reader must never ask a phone to pair, because the walk-up unlock depends on it not asking. Setting the permission to open instead hands an unauthenticated peer a write path into flash, and mcumgr's OS group would hand it an unauthenticated reset command as well. A lock anyone in radio range can reboot in a loop is a real attack. So the patch rides the CoC transport this board already has, on its own PSM, and authorization is a WINDOW rather than a handshake. WHY A WINDOW IS ENOUGH. The gate is a denial-of-service control, not an integrity one. The patch header is signed and the application checks it (modules/woz_dfu/src/dfu_receiver.c), and underneath that MCUboot re-verifies the P-256 signature of the patched RESULT before booting it. No peer can install code no matter what reaches this channel. What a closed channel prevents is a stranger spending the flash's erase cycles and the owner's uptime.matter_thread_port.cmatter_thread.h on top of Zephyr's OpenThread. The dataset arrives from the commissioner as raw meshcop TLVs and otDatasetSetActiveTlvs() takes raw meshcop TLVs, so nothing here has to understand the format -- which is the point. This node parses exactly one field out of it, the Extended PAN ID, and only so it can name the network back to the commissioner. Built into every image. Without CONFIG_OPENTHREAD it refuses honestly rather than disappearing: matter_clusters.c calls it unconditionally, and a link error would be a worse way to learn that Thread was configured out.prov_shell.cstatus_led.cShow that the update window is open, on the board itself. The window is the entire authorization model for an update, and until now it was invisible. Three things open it -- SW2, Apple Home's "Turn On Pairing Mode", and the bench SWD write -- and none of them gave the board any way to say so. An owner who pressed the button could not tell whether the press had registered, and the five minutes could run out while they were still finding the phone. The only feedback was a log line on a debugger that a released board does not have attached. D10, the blue one, at 2 Hz. Blue because the other three are the DW3000's own colours by convention on this board (D13 is tx red / rx green) and a fourth red would read as a fault; 2 Hz because a slower heartbeat reads as "alive" rather than "waiting for you", which is the wrong message for something that expires. It follows the window rather than the button, so it is honest about the state that actually matters: it goes out when the window expires on its own, not when someone stops pressing.thread_gate.c
hosthost/presence5 modules
presence-runNo module docstring. First commit: "presence: add local daemon and command gate".presence-enrollNo module docstring. First commit: "presence: add local daemon and command gate".presencedNo module docstring. First commit: "presence: add local daemon and command gate".presence_client.pyClient and command gate for the local presenced Unix socket.presence_service.pyFresh, pinned presence proofs behind an owner-only Unix socket.
integrationintegration/homeassistant22 modules
__main__.pyModule entry point for the HA=1-only staging command.__init__.pyHA=1-only staging library for the OpenAliro Home Assistant adapters.__init__.pyHA=1-only OpenAliro direct-serial integration.diagnostics.pyRedacted diagnostics for the HA=1 OpenAliro direct integration.event.pyAccess outcome event entity for the HA=1 OpenAliro direct integration.sensor.pyDistance sensor for the HA=1 OpenAliro direct integration.aliro_mqtt_bridge.pyRepublish the lock's console log to MQTT as Home Assistant entities.config_flow.pyManual direct-serial config flow for the HA=1 OpenAliro beta.device_trigger.pyGranted and denied device-automation triggers for OpenAliro access events.cli.pySmall, non-interactive HA=1 staging CLI for safe offline operations.agent.pyRunnable standalone-agent orchestration over the shared serial library.compatibility.pyIncremental parser for the source-proven `aliro range` compatibility mode.config.pyVersioned, secret-free TOML configuration for the HA=1-only agent.models.pyTyped observations emitted by the HA=1 console parser.mqtt.pyStandalone MQTT adapter for the HA=1 staging agent.parser.pyNarrow parser for the currently verified nRF5340 console output.serial_session.pyAsync, transport-neutral ownership of one OpenAliro serial console.serial_transport.pypyserial adapter and privacy-safe serial-port identity helpers.const.pyConstants for the HA=1 OpenAliro custom integration.runtime.pyHome Assistant runtime bridge over the shared OpenAliro serial session.ha-setup.shOne command from nothing to a working OpenAliro Home Assistant agent. Generates the broker TLS material, installs it into the Home Assistant Mosquitto add-on over SSH, writes the agent configuration, and runs doctor. Every step is idempotent: re-running repairs whatever drifted. Override any default with an environment variable, for example HA_SSH=my-hass BROKER_HOST=hass.lan ./ha-setup.shpackage_component.pyBuild a local OpenAliro custom-component archive without publishing it.
portsports37 modules
app_main.cppMatter application main: door lock endpoint setup, Matter lifecycle event handling, and (when CONFIG_ENABLE_ALIRO_BLE_UWB is set) startup/coexistence wiring for the Aliro BLE+UWB reader alongside the Matter BLE commissioning transport. Owns the Aliro reader background task (started once on commissioning-complete or at boot if already commissioned) and the Matter attribute/identify/device-event callbacks required by esp-matter's node/cluster framework.app_shell.cppESP32-IDF console shell for the Aliro Matter door lock app: registers status, range, aliro, lock/unlock, codes, factoryreset, and clear commands and runs the REPL.piv_ccid_usb.cNo module docstring. First commit: "piv: add ESP32-S3 CCID bench transport".piv_identity.cNo module docstring. First commit: "piv: gate macOS unlock on fresh presence".app_driver.cppBoard driver glue for the ESP32 Matter port: button input, WS2812 lock-status LED, and the Matter attribute-update hook wired into the app's driver layer.app_shell.cESP32-IDF console shell for the standalone Aliro UWB responder bench app: registers status, range, aliro-start/stop, provisioning, trust, and clear commands and runs the linenoise-based REPL.main.cWoz UWB ranging engine on ESP32 (ESP-IDF) — minimal bring-up app. Binds a canned URSK and starts the CCC DS-TWR responder on the DW3000, then polls for a range. With no iPhone/initiator present this proves the SPI + DW3000 + CCC init path comes up; a live range needs a peer that drives the DS-TWR exchange (an Aliro Wallet, or a second board as initiator). The demo responder lifecycle + interactive console live in app_shell.c.piv_ccid.cNo module docstring. First commit: "piv: add ESP32-S3 CCID bench transport".ha_mqtt.cNative Home Assistant MQTT publisher for the ESP32 Matter lock — see ha_mqtt.h for the wire contract this holds with integration/homeassistant.aliro_reader_delegate.cppAliroReaderDelegate: implements the Aliro reader-provisioning and BLE-UWB portions of the Matter DoorLock::Delegate interface, backing the controller-facing GetAliro*/SetAliroReaderConfig commands and persisting the provisioned reader identity via aliro_reader_provision_identity. Bridges Matter cluster commands to the underlying aliro_reader NVS-backed identity/trust store and to the BLE advertising layer (refreshed when the group resolving key changes).door_lock_callbacks.cppMatter DoorLock cluster plugin callbacks: wires the ESP32 port's BoltLockManager into the Matter DoorLock cluster's lock/unlock commands, user and credential storage, schedule storage, cluster init, and auto-relock notification hooks.door_lock_manager.cppBoltLockManager: Matter door lock cluster backing store for the ESP32 port. Implements the DoorLock cluster's user, credential, and weekday/yearday/holiday schedule get/set callbacks over fixed-size in-memory tables mirrored to NVM (ESP32Config blobs), plus lock/unlock actuation and PIN validation. Cluster indices are one-indexed by Matter and decremented internally before bounds-checking against this platform's fixed capacity limits.lock_led.cLock-state indicator LED: maps lock state (and Aliro activity) to an RGB colour for the single status pixel. Locked always extinguishes the indicator; unlocked shows blue during active UWB/Aliro engagement and a different colour otherwise, per lock_led_color.presence_link.cPresence dongle commands (see presence_link.h).proveends every old Aliro link, waits for a new trusted credential authentication and a later trusted UWB range, then signs that post-challenge result under a persistent P-256 key. These live on the ordinary console rather than a private binary channel, so one board can be provisioned (aliro-import) and queried for presence without reflashing between modes, and so a stray log line is just another line instead of a corrupted frame.piv_apdu.cNo module docstring. First commit: "piv: add ESP32-S3 CCID bench transport".dw3000_hw.cESP-IDF GPIO/IRQ backend for the DW3000 decadriver — implements dw3000_hw.h. Replaces the Zephyr deps/dw3000/platform/dw3000_hw.c (not compiled here). IRQ mirrors the Zephyr design: the GPIO ISR wakes a dedicated high-priority task that calls dwt_isr() while the IRQ line stays high — dwt_isr does SPI, so it cannot run in true ISR context. Dual-core targets pin the worker to core 1; single-core targets run it on core 0. Also provides the cycle-counter diag symbols that the decadriver references via esp_cpu_get_cycle_count().dw3000_spi.cESP-IDF SPI backend for the DW3000 decadriver — implements dw3000_spi.h. Replaces the Zephyr deps/dw3000/platform/dw3000_spi.c (not compiled here). CS is a plain GPIO (spics_io_num = -1), matching the Zephyr cs-gpios model, so the wakeup path can hold CS low ~500us. Each DW3000 command is one CS-low full-duplex transfer: header + body assembled in a DMA-capable, word-aligned bounce buffer; on reads the body slice of the RX buffer is copied back.app_priv.happ_shell.hha_mqtt.hNo module docstring. First commit: "Publish to Home Assistant MQTT natively from the ESP32 lock".aliro_reader_delegate.hDeclares AliroReaderDelegate, the Aliro (Apple Home Key) reader-provisioning and BLE-UWB half of the Matter DoorLock cluster delegate, bridging controller commands to the on-device reader identity, trust store, and BLE advertising state.door_lock_manager.hDoor lock manager for the Matter DoorLock cluster: owns bolt lock state plus the users, credentials, and weekday/yearday/holiday schedules backing the cluster's server attributes. Declares BoltLockManager (accessed via the BoltLockMgr() singleton) and the LockInitParams::LockParam/ParamBuilder types used to configure it from zap-derived capacity attributes at init time.presence_link.hPresence dongle commands (CONFIG_WOZ_PRESENCE): fresh, challenge-driven signed statements from a new trusted Aliro authentication and later UWB range, turning proximity of a provisioned iPhone into a factor any tool can check. See tools/presence_verify.py and tools/presence_git.py for the other end. These are console commands rather than a private binary channel, so the shell stays available on the same board: provisioning (aliro-import) and presence both work without reflashing between modes. Every response is one tagged hex line, so a log line landing mid-conversation is just another line rather than corruption: presence pub -> PRESENCE-PUB <65 bytes hex> (enrolment) presence credential -> PRESENCE-CRED <8 bytes hex> (pinned human) presence prove <nonce-hex> -> PRESENCE-P256 <115 bytes hex> (fresh proof) anything rejected -> PRESENCE-ERR <reason>piv_ccid_usb.hNo module docstring. First commit: "piv: add ESP32-S3 CCID bench transport".piv_ccid.hNo module docstring. First commit: "piv: add ESP32-S3 CCID bench transport".piv_identity.hNo module docstring. First commit: "piv: gate macOS unlock on fresh presence".lock_led.hLock status LED color mapping: derives the RGB color for the lock indicator from the current locked and Aliro-ranging state.app_shell.hpiv_apdu.hNo module docstring. First commit: "piv: add ESP32-S3 CCID bench transport".board_pins.hDW3000-family wiring per ESP32 target, SPI2/FSPI. Source of truth for the wiring table in docs/esp32-bringup.md. Change to match how the UWB module is wired to your board.main.cESP32-S3 application entry for the Aliro initiator, the User-Device role that stands in for an iPhone on the bench. Starts the NimBLE central, which scans for the reader's 0xFFF2 advert, connects, reads the reader's SPSM, supported versions and features, writes the version it selects, and opens the L2CAP channel. It then runs the Access Protocol over that channel: every inbound AUTH0/AUTH1/EXCHANGE command is fed to the device state machine and the sealed response is framed straight back, ending in the same 32-byte URSK the reader derives. Credentials are the compiled-in bench pair below, which works only against a reader running its dev identity with an empty trust store.aliro_ble.cNimBLE-backed BLE transport for the Aliro reader: GAP advertising, the Aliro GATT service, and an L2CAP connection-oriented channel (CoC) used to carry Aliro protocol messages. Supports two bring-up modes: a standalone NimBLE host (aliro_ble_start) and attachment to a host already owned and synced by another stack such as esp-matter (aliro_ble_prepare + aliro_ble_start_attached). Tracks CoC channels per connection handle in a fixed-size table and exposes send/receive plus reader-status notification helpers to the rest of the Aliro reader.aliro_ble_central_nimble.cNimBLE central/client backend for the Aliro initiator: the mirror of components/aliro_ble/aliro_ble.c. That file advertises 0xFFF2, serves the characteristics and runs a CoC server; this one scans for 0xFFF2, connects, discovers, reads the reader's SPSM/versions, writes the selected version and opens a CoC client to that SPSM.aliro_prov_nvs.cNVS-backed persistence for Aliro reader provisioning: loads and stores the serialized reader identity and trust store built by aliro_prov.c. Lazily initializes NVS on first use; safe to call alongside aliro_ble's own nvs_flash_init.aliro_stepup_worker.c@file aliro_stepup_worker.c Step-up document verification worker for ESP32. Runs on a dedicated FreeRTOS task (6 KB stack, priority 4). Lazily creates a single-slot queue on first submission. Non-blocking submission: if a previous job is still enqueued, the new job is dropped. Verdict and connection handle are stored in shared state (spinlock-protected) and retrieved via aliro_stepup_worker_last(). Logging includes decrypted DeviceResponse hex and verdict breakdown (validity, element count, issuer found, signature OK, doctype OK, time OK, iteration OK).woz_seam_stubs.cThis port's half of modules/woz_uwb/src/driver/uwb_seam.h. The Nordic build routes DW3000 RX events through uwb_rxdiag.c's woz_uwb_set_callbacks -> shim_rxok, which (after the MAC's own prepoll_rx_rearm arms the SP3 POLL window) calls ccc_shim_rx_try_prepoll to decrypt+warm the NEXT block's STS. That bootstrap warm is what flips g_warm_valid true so the POLL window ever gets armed and Response_0 sent. This port omits uwb_rxdiag.c wholesale (its heartbeat needs Zephyr k_work, which the compat layer does not provide), so without this shim the callbacks reach the radio unmodified, ccc_shim_rx_try_prepoll is never called, g_warm_valid stays false, and the responder receives Pre-POLLs but never replies. Re-create only the essential chain here (no k_work, no diagnostics), plus the PHY-config seam, which this port has nothing to add to.main.cnRF5340DK on-target self-test for the Aliro device (initiator) EC path: a minimal Zephyr application that brings up the real PSA backend (nrf_security on CryptoCell), runs the same credential-auth crypto suite the host tests run, and prints PASS or FAIL to the DK console. It exists because the host suite proves the maths against a software curve only; this proves the same vectors on the silicon that will ship, and it caught a PSA import failure that no host run could see. Crypto only: no BLE, no UWB, no iPhone.
releaserelease3 modules
flash.shflash.sh — program the openaliro DWM3001CDK firmware over its on-board J-Link. See FLASH.md for the full walkthrough. Usage: bash flash.sh [JLINK_SERIAL_NUMBER] One image, not two: the nRF52833 is a single-core part, so unlike the nRF5340 DK there is no separate network-core hex to write.flash.shflash.sh — write the openaliro ESP32 Matter lock to a board with esptool. One merged image (bootloader, partition table and app) at offset 0x0. See FLASH.md for wiring and first run. Usage: bash flash.sh [--chip esp32s3|esp32c5|esp32c6] [PORT] bash flash.sh ask which chip, let esptool find the port bash flash.sh --chip esp32c6 no question bash flash.sh --chip esp32s3 /dev/ttyACM0 The bundle ships an image for each of three chips, and writing the wrong one gives a board that flashes cleanly and then never boots. So the chip is asked for rather than assumed: this script used to hardcode the S3 and ignore the other two images entirely.flash.shflash.sh — program both cores of the openaliro nRF5340 DK firmware with nrfutil. Goes over the DK's on-board J-Link. See FLASH.md for setup and first run. Usage: bash flash.sh [JLINK_SERIAL_NUMBER]
scriptsscripts38 modules
bootstrap.shbootstrap.sh — build a self-contained west workspace, PRISTINE from upstream. Fetches everything the build needs from public GitHub into ./workspace (git-ignored), then applies our integration patches on top. It never reads from any other local checkout — a clean upstream fetch every time. Fetches (all public): - Nordic add-on ncs-door-lock-and-access-control @ the pin below - NCS v3.3.0 + Zephyr + every module (via the add-on's own west manifest) The NCS v3.3.0 toolchain it needs is installed here too, once per machine, so a clone reaches a build in one command instead of three. Usage: scripts/bootstrap.sh # workspace in ./workspace ALIRO_WS=/big/disk/ws scripts/bootstrap.sh # put the multi-GB workspace elsewherebuild-nrf5340dk.shbuild-nrf5340dk.sh {build|rebuild|flash|flash-erase|build-flash} — build the Aliro NFC+UWB image for the nRF5340 DK from the self-contained ./workspace. Run scripts/bootstrap.sh first. Named for its board because BOARD below is hardcoded: this script builds nrf5340dk/nrf5340/cpuapp and nothing else. The DWM3001CDK is built straight from firmware/ by mk/cdk.mk, and the ESP32 apps by mk/esp32.mk. Layers our modules + ISC dw3000 onto the fetched add-on via out-of-tree overlays. Output → build/nrf5340dk (git-ignored), or build/nrf5340dk-blob when ALIRO_SOURCE=0, so flipping that flag no longer forces a pristine rebuild. Incremental by default — a full from-scratch (pristine) build runs only when it has to: first build, changed build flags (UWB chip / self-test / config), or when you ask for one. A preflight first checks the workspace is bootstrapped. scripts/build-nrf5340dk.sh build # incremental where safe (fast) scripts/build-nrf5340dk.sh rebuild # force a clean pristine build PRISTINE=1 scripts/build-nrf5340dk.sh build # same as rebuild UWB_SELFTEST=1 scripts/build-nrf5340dk.sh build # one-shot boot self-test, no iPhone (diagnostic) PRETTY=1 scripts/build-nrf5340dk.sh build # curated/clean console (reversible; default verbose) ALIRO_SOURCE=0 scripts/build-nrf5340dk.sh build # legacy Nordic Aliro binary fallback UWB_CHIP=dw3720 scripts/build-nrf5340dk.sh build # select the plugged-in UWB chip (default: dw3000) LTO=1 scripts/build-nrf5340dk.sh build # link-time optimisation (overlays/lto.conf) DFU=1 scripts/build-nrf5340dk.sh build # MCUboot + Matter OTA (overlays/sysbuild-dfu.conf) NOTE both default to OFF *here* and ON viamake nrf-build, which is the same split the DWM3001CDK uses: mk/ is the policy layer and decides what a plain build means, this script only does what it is told. Call it directly and you get neither unless you ask. DFU=1 needs this checkout's image-signing key (make dfu-key) and refuses to build without one, because a bootloader that trusts MCUboot's published demo key trusts everybody. SIGN_KEY=<absolute path> overrides where it looks.cdk-dfu.shcdk-dfu.sh — push a signed image to the DWM3001CDK over MCUboot serial recovery. WHY THIS IS A SCRIPT AND NOT A MAKE RECIPE. The ordering below is the whole job and a recipe got it wrong twice. MCUboot listens for an mcumgr command for a fixed window after reset and then boots the application; miss the window and the port answers nothing, which looks exactly like broken wiring. So the probe loop has to already be RUNNING when the reset lands. Backgrounding the reset instead does not work: nrfjprog spends seconds connecting to the probe before it pulls the line, by which time the window has opened and shut. NO BUTTON IS NEEDED, but not for the reason an earlier version of this comment gave. SW1 DOES reset this board: UICR.PSELRESET reads 0x00000012 (pin 18, CONNECT clear), and a tap produces a full fresh boot on RTT. Check it with nrfjprog --memrd 0x10001200 --n 8 CONFIG_GPIO_AS_PINRESET only WRITES that field, so grepping an app's .config for that symbol says nothing about whether the pin currently resets. This resets over SWD anyway, because that needs no operator and no timing. WHAT DOES NOT WORK, AND IS NOT UNDERSTOOD. Serial recovery completed exactly one real upload (2026-08-02 ~22:00) and has not been reproducible since, on the same config, binary and board. Ruled out by measurement, none of them the cause: the window duration (400 ms and 30000 ms fail identically), a J-Link session blocking the VCOM (a cold boot with no debugger fails too), a stale process on the port, a wedged probe, board health, the provisioned state, and the reset mechanism. Also verified WORKING: UART TX (3,392 bytes out of the app), UART RX electrically (EVENTS_RXDRDY=1 and ERRORSRC=0x1 after 200 bytes in), the pinctrl in both images, and MCUboot reaching its wait window at all (the application appears ~5 s after reset, which is the window elapsing). So MCUboot sits in its window on a working UART and does not answer. The next measurement worth taking is instrumenting MCUboot itself rather than inferring it from outside: CONFIG_MCUBOOT_INDICATION_LED with an mcuboot-led0 alias, or logging over RTT, to see whether boot_serial_check_start is entered and with what timeout.cdk-rtt-elf-check.shRefuse to attach RTT with an ELF the board is not running. probe-rs reads the _SEGGER_RTT control-block address out of the ELF you hand it. Hand it one you built but did not flash and it reads an address the board never populated, then prints nothing -- which looks exactly like a dead board. That failure has cost real bench time, somake monitorchecks first. The predicate is the _SEGGER_RTT address, not the file bytes. Two ELFs that place the control block identically stream fine no matter how else they differ, and a byte compare would refuse those too -- false refusals are how a guard gets routed around. Exit 1 ONLY on a positive mismatch: two addresses that were both read and disagree. Anything that leaves the question open (no record of a flash, no toolchain nm, no symbol) warns and exits 0, because blocking a console on an indeterminate check is worse than the bug. Usage: cdk-rtt-elf-check.sh <candidate-elf> <deployed-elf>cdk-size-baseline.pycdk-size-baseline.py — turn a size report into the committed baseline.cdk-size-compare.pycdk-size-compare.py — head against the recorded baseline, as a gate.cdk-size-notify.pycdk-size-notify.py — say what a change cost the CDK image, in Discord.cdk-size.pycdk-size.py — what the DWM3001CDK image costs, as a machine-readable record.check-approtect.shcheck-approtect.sh — refuse to ship an image that locks APPROTECT. WHAT IS BEING PREVENTED. On the nRF52833 (and the nRF5340), selecting CONFIG_NRF_APPROTECT_LOCK makes SystemInit() lock the firmware branch of the APPROTECT mechanism on EVERY boot, before any of our code runs. The only way back isnrfjprog --recover, which mass-erases flash AND UICR. On this project that is not "lose the firmware" -- it is: * settings_storage (0x7e000) gone, so the Matter fabrics and trust anchors go * the reader private key gone (firmware/src/prov_shell.c), and EVERY iPhone key already provisioned against this board dies with it A board that has done this is not bricked, but every future debug session costs a full wipe and a re-provision, and the credentials cannot be recreated. NCS defaults to open (NRF_APPROTECT_USE_UICR); the requirement is only that nobody turns it on. This gate is what makes "nobody" true. scripts/check-approtect.sh # the two config layers scripts/check-approtect.sh --device SNR # what the attached board is ACTUALLY in scripts/check-approtect.sh --self-test # prove the gate can actually fail make verify # runs this as theapprotectgate Exit 0 clean, 1 on a finding, 2 if the gate could not do its job. THREE LAYERS, because any one alone is a gate that passes while checking nothing: sources Every tracked config file. This is the layer that works in CI, which never builds firmware (firmware-builds.yml is workflow_dispatch only), so a .config scan there would find zero files and report success against nothing. generated Every */zephyr/.config that exists locally. This is the layer that catches what the source scan CANNOT: the setting arriving from a board defconfig, an SoC Kconfig default, or a sysbuild set_config_bool -- none of which appear anywhere in this tree. Checking the generated config is the only way to know what was actually compiled, which is why the source layer never stands in for it. device What the SILICON is in right now, read back over the probe. Opt-in (--device SNR) because it needs a board attached. The generated layer reporting "0 builds examined" is NOT a pass and is not silent: it says so, and it is the reason the source layer is not optional. WHY THE DEVICE LAYER EXISTS, which is the expensive lesson. Both config layers answer "did our firmware ask for the lock". Neither answers "is this board locked", and those come apart: a mass erase leaves UICR blank, and on the nRF5340 a blank UICR reads as APPROTECT ENGAGED until firmware writes it open again. On 2026-08-03 an nRF5340 DK sat in exactly that state while this gate reported "3 generated image config(s) examined, all open", which was true and useless. The probe then served partial reads: RAM below ~0x20057000 read back fine and everything above it returned "memory protection issue", which reads exactly like a board with 100 KB of RAM missing. Hours went into a hardware theory for what was a protection state, andnrfutil device recovercleared it in one command. Ask the silicon.check-signing-key.shcheck-signing-key.sh — refuse to build a bootloader that anybody can sign for. WHAT IS BEING PREVENTED. MCUboot boots slot 0 only if the image verifies against a public key compiled into the bootloader, so the private half is the whole answer to "what firmware will this lock run". Configure nothing and MCUboot signs with root-ec-p256.pem out of its OWN repository, where that key is published. Every stock MCUboot in the world accepts images signed with it. On a lock that is not a signing key, it is a formality. MCUboot does notice, at bootloader/mcuboot/boot/zephyr/CMakeLists.txt:449-452, and calls message(WARNING). That is precisely why it survived on this port for as long as it did: a warning in a ten-thousand-line build log is indistinguishable from no warning. Here it is fatal. scripts/check-signing-key.sh <path> # validate one configured key scripts/check-signing-key.sh --self-test # prove the refusals actually fire Exit 0 clean, 1 on a finding, 2 if the gate could not do its job. Both Zephyr ports call this, which is why it is a file rather than a paragraph repeated in each: firmware/sysbuild.cmake for the DWM3001CDK, and scripts/build-nrf5340dk.sh for the nRF5340 DK. One list, one set of refusals, one place to edit when upstream adds an eighth demo key. The DK additionally reads the key back out of the built mcuboot .config, because a flag we passed is not the same fact as a flag the build honoured.check-uwb-seam.shcheck-uwb-seam.sh — keep the CCC STS seam impossible to bypass. WHAT IS BEING PREVENTED. Four decadriver entry points carry engine behaviour that a caller must not skip (modules/woz_uwb/src/driver/uwb_seam.h): dwt_rxenable arming RX must first program the CCC STS for the slot dwt_configurestsiv loading an STS-IV must substitute the CCC STS-V dwt_setcallbacks registering callbacks must insert the Pre-POLL shim, which is what warms the next block's STS at all dwt_configure a PHY (re)configuration is traced A call site that reaches past the seam is SILENT on the bench: the radio still arms, ranging still runs, the phone just never unlocks anything because the STS never matched. That is a bad afternoon to debug, and it is exactly the failure mode a link-time interposer used to make structurally impossible. This gate buys that guarantee back mechanically. scripts/check-uwb-seam.sh # scan the tracked sources scripts/check-uwb-seam.sh --self-test # prove the gate can actually fail make verify # runs this as theuwb-seamgate Exit 0 clean, 1 on a finding, 2 if the gate could not do its job. WHAT IS EXEMPT, and why each one is not a hole: uwb_seam.h declares the helpers; the non-engine tier inlines straight to the driver, which IS the fallback ccc_shim_rx.c implements woz_uwb_arm_rx. Its own self-rearm ccc_shim_wrap.c implements woz_uwb_set_sts_iv sites have uwb_rxdiag.c implements the other two already port/woz_seam_stubs.c the ESP32 half of the same two programmed the STS ccc_sts.c the register-level key/IV packer itself, with no production caller (host suites only) deps/dw3000/** the vendor decadriver: it defines these tests/**, ports/esp32/test/**, docs/** host doubles and prose Adding a file here is a decision to trust it forever. Prefer calling the seam.deadcode-codechecker.shdeadcode-codechecker.sh — CodeChecker over the real firmware build. Same target as deadcode-tidy.sh and the same database, but it runs the Clang Static Analyzer as well as clang-tidy, keeps results in a store so two runs can be diffed, and writes an HTML report. Use deadcode-tidy.sh for the quick pass; use this when you want the cross-translation-unit analyser or a report to read. It reuses the FILTERED database that deadcode-tidy.sh writes, because the raw Zephyr one is GCC-flavoured and clang rejects several of its flags outright. Running the tidy script first is therefore not optional, and this checks.deadcode-graph.shdeadcode-graph.sh — find functions nothing calls, using the documate code graph. Why this exists rather than -Wl,--print-gc-sections: that flag lists what the linker THREW AWAY, which by definition is not in flash. The dead code worth finding is what survives gc-sections because something references it without ever calling it -- a function in an ops table, a callback registered into a struct nobody dispatches. deps/dw3000's interface_rx_enable was exactly that: present in the shipped ELF, zero callers, kept because a dwt_mcps_ops table names it. No linker flag can see that; a call graph can. Three tiers, because "the graph shows no callers" is not evidence of death: A zero inbound CALLS, referenced nowhere else in the tree, AND absent from the unindexed upstream. The only tier worth calling a candidate. B zero inbound CALLS, but referenced somewhere in-tree -- an ops table, a SYS_INIT/SHELL_CMD registration, a header declaration. Zephyr registers through linker arrays constantly, so most of tier B is alive. This is NOT a delete list; it is where table-registered dead code hides, and reading the reference is the only way to tell which. U zero inbound CALLS in-tree, but the fetched upstream calls it. Live API. Tier U exists because the first version of this script did not have it and proposed deleting nine woz_aliro_stack methods -- the module reimplements the Nordic Aliro API, and every one of them is called from workspace/ncs-door-lock-and-access-control, which documate does not index. CLAUDE.md warns about exactly this: fetched upstream is not in the graph. Without a workspace to check, tier A is unverifiable and says so. Needs .documate/graph.db, whichmake docsbuilds and .gitignore excludes.deadcode-size.shdeadcode-size.sh — flash cost of the functions nothing calls. deadcode-graph.sh answers "what has no callers". This answers "and what does that cost", by joining that list against the symbol sizes in the linked image. A zero-caller function that the linker already discarded costs nothing and is not worth an argument; one that survived into .text is real flash. That distinction is the whole reason -Wl,--print-gc-sections is the wrong tool for this: it lists what was REMOVED. What is in the image and unreachable never appears in its output. ./scripts/deadcode-size.sh rank uncalled symbols by flash bytes ./scripts/deadcode-size.sh --serve puncover's interactive view instead puncover renders callers/callees and stack depth per symbol from the DWARF, which is worth more than any text report once you are chasing a specific function. It is a server: it does not exit, so it is not scriptable. Its --generate-report writes stack-usage entries only, not symbol sizes.deadcode-tidy.shdeadcode-tidy.sh — run clang-tidy against the REAL firmware build. scripts/verify.sh already has a clang-tidy gate, but it compiles UNIT_SRCS out of tests/host/sources.sh with host flags: -std=c11, a macOS sysroot, and the host fakes. That covers six modules and nothing else. firmware/src and modules/woz_dfu are in none of it, which security/semgrep-parse-baseline.txt already records as a gap -- and modules/woz_dfu parses signed update payloads arriving over Bluetooth. This runs the same tool against build/<img>/compile_commands.json instead, so the analysis sees the actual Cortex-M4 target, the real include paths and the generated autoconf.h, rather than a host approximation of them. Two things have to be fixed before clang can read a GCC database: 1. GCC-only flags are hard errors to clang ("unknown argument"), not warnings, so one of them kills the whole file. They are stripped below. The list is deliberately explicit: a silent catch-all would also swallow a flag that changes semantics. 2. Zephyr's generated autoconf.h defines negative Kconfig values bare (#define CONFIG_SYSTEM_WORKQUEUE_PRIORITY -1), which trips bugprone-macro-parentheses ~1000 times per file. The header filter keeps findings to this repo's own sources.docs-sync.shdocs-sync.sh — put the generated artifacts back in step after a merge. The committed docs are generated, so when a merge brings in someone else's regeneration they conflict on their derived lines: a subsystem count, a coverage percentage, a table row. Both sides are right about their own tree and both are wrong about the merge, so no resolution is a merge. The only correct output is a fresh generation, which is what this does. The order is the whole point, because each step invalidates the next: 1. take our side of any conflicted generated file, so the tree parses again 2. drop the parse cache, which otherwise replays pre-merge line numbers that look plausible and are wrong 3. regenerate docs/, which MOVES line numbers inside docs/ARCHITECTURE.md 4. rebuild the spec index, which cites four of those line numbers Running 4 before 3 anchors the index to lines the docs build is about to move. That passes locally and fails in CI, which is the trap this script exists to close. Run it throughmake sync. SYNC_NO_VERIFY=1 stops before the sweep, for when you have another reason to run it yourself.docs.shdocs.sh — build the documentation site into site/. Two generators write into the same output directory, in this order: 1. the subsystem tree + guides + search shell -> site/*.html 2. doxygen (docs/Doxyfile) -> site/api/ then a link pass rewrites cross-document links so the published site has no dead ends, and the freshness gate confirms the committed docs/ tree matches the source. Run it throughmake docs. Nothing here needs the NCS toolchain or hardware.flash_html.pyRender a release FLASH.md into a self-contained FLASH.html.presence_runtime.pyBuild the minimal, deterministic presence runtime transfer archive.release-bundle.shrelease-bundle.sh — assemble one publishable firmware bundle. scripts/release-bundle.sh --target dwm3001cdk --out build/release/... \ --version v0.5.0 --board 'DWM3001CDK (nRF52833)' \ --setup-code 12345678 merged.hex Options: --target <slug> release/<slug>/ supplies the guide and script --out <dir> destination, wiped and recreated --version <text> the tag, orgit describewhen omitted --commit <sha> defaults to HEAD --board <text> hardware line in VERSION.txt --setup-code <code> Matter setup code, when the build knows it --commission-note <text> the line printed under it, or instead of it Writes the firmware given as positional arguments, plus flash.sh, FLASH.md, FLASH.html and README.txt from release/<slug>/, plus a generated VERSION.txt and SHA256SUMS.txt. Every bundle gets all of them: this is the one place that decides what a release zip contains, so the three targets cannot drift. Exit 0 on a complete bundle, 1 on any failure. There is no partial success — a bundle missing a file looks identical to a good one once it is a zip.release-notes.shrelease-notes.sh — render the GitHub release body from release/NOTES.md.in. scripts/release-notes.sh v0.5.0 # preview it scripts/release-notes.sh v0.5.0 out/SHA256SUMS.txt # what CI publishes Placeholders: @TAG@ @REPO@ @PAGES@ @CHANGELOG@ @SUMS@ Env: REPO=owner/name (default openaliro/openaliro) These notes are also the release email: GitHub renders them into the notification it sends watchers, so the checksums stay inside a <details> and nothing load-bearing sits below the fold.security-attest.shsecurity-attest.sh — can somebody who downloaded a release prove where it came from? Yes, since release.yml grew an attest-build-provenance step. It did not always: SHA256SUMS.txt on its own answers "are these the bytes the release page listed" and not "did this repository's CI build them". Those are different questions, and the second is the one that matters for a project whose distribution path ends in a browser page calling navigator.serial. A SHA256SUMS.txt served from the same release as the artifacts it describes is signed by nothing; whoever could replace the .bin could replace the sums file in the same motion. The fix was one action and two permissions in release.yml (see INTEGRATION.md), producing a Sigstore-backed attestation that binds each artifact to the workflow, repository and commit that built it. This script is the other half: the part that runs outside CI and checks the CI half is real. The subject list covers the unzipped bundle contents as well as the zips, which is what lets each release/<target>/flash.sh verify the exact image it is about to write to a board. A guarantee nobody can reach at the moment they need it is not much of a guarantee. scripts/security-attest.sh workflow # static: release.yml still emits attestations scripts/security-attest.sh verify v0.4.0 # download a release and verify it end to end make security-attest Two modes, because they answer to different failure modes.workflowneeds no network and no release to exist, so it can sit in the fast lane and catch the attestation step being dropped in an edit — the way a security control usually dies.verifyis what a user would run, and is the only thing that proves the control works rather than that it is configured. Exit 0 clean, 1 on a finding, 2 if the mode could not run. Env: REPO=owner/name default openaliro/openaliro NO_COLOR=1 plain outputsecurity-ct.shsecurity-ct.sh — secret-dependent branches and table lookups in the CCC key ladder. Every other gate in this repo asks whether the code computes the right answer. This one asks whether it takes the same amount of time doing it, which no test, no sanitizer and no fuzzer in the tree can see: a KDF that early-outs on a key byte passes every existing check with a green tick, and hands an attacker the key one byte at a time. The mechanism is ctgrind's, and it is almost free. Memcheck already reports a branch or an array index that depends on undefined memory. Poison the URSK instead of leaving it uninitialised and that same report becomes "this branched on the key". Nothing new is instrumented; the harness (tests/host/ct/ct_main.c) just marks the secret and runs the ladder. Scope, stated up front because a green run means nothing without it: the AES primitive is suppressed. tests/host/aes_ref.c is an S-box implementation and is variable-time by construction, and it is not the primitive that ships — nRF5340 uses CryptoCell through PSA, ESP32 uses mbedTLS over the AES peripheral. So this gate covers the ladder and the SP0 wrapper, which is the code this project wrote, and says nothing about the cipher underneath, which it did not. tests/host/ct/host-aes.supp is where that boundary is drawn. scripts/security-ct.sh # build + run under memcheck CT_DOCKER=1 scripts/security-ct.sh make security-ct On Apple silicon there is no valgrind, and there will not be one. That is a real hole in the pre-push sweep rather than something to paper over, so the script says so on stdout, exits 2 (distinct from a finding's 1), and offers CT_DOCKER=1 to run the identical command inside the linux/amd64 image CI uses. verify.sh turns the 2 into a row that reads "not run here, runs in CI", the same shape cbmc already has. Env: CT_DOCKER=1 run inside docker (linux/amd64) instead of natively CT_CC=clang compiler (default: cc) NO_COLOR=1 plain outputsecurity-diff.shsecurity-diff.sh — the structural half of the malicious-change gate. security/semgrep-malicious.yml asks what a diff SAYS. This asks what a diff DOES to the shape of the tree: a binary appearing where only source lives, a file quietly gaining its executable bit, a symlink pointing out of the checkout, a submodule nobody discussed, a capture file that SECURITY.md says carries the session URSK. None of those are expressible as a source pattern, because in every case the payload is opaque to a text scanner — that is the point of using them. So they are checked here, againstgit diff --raw, which reports mode and blob type whatever the bytes happen to be. scripts/security-diff.sh # merge-base with origin/main .. HEAD scripts/security-diff.sh <base> # <base> .. HEAD scripts/security-diff.sh <base> <head> # explicit range, what CI passes Exit 0 clean or warnings only, 1 if anything blocking was found, 2 on bad usage. Two severities, and the split is deliberate. BLOCK is for changes with no legitimate form in this repository — checked against the tree as it stands, which has zero symlinks, zero gitlinks, five binary files (two in assets/, three fuzz corpus seeds) and thirty executables that are every one of them a shell or python script. WARN is for changes that are usually fine but are worth a reviewer's eye: a new dependency, a workflow edit, a new remote URL. Warnings do not fail the gate, because a gate that cries wolf on a Dependabot bump is a gate that gets bypassed, and then the blocking half goes with it. Env: SECDIFF_MAX_KB=512 size above which an added file is blocking (see BINARY_OK_DIRS) NO_COLOR=1 plain outputsecurity-fw.shsecurity-fw.sh — the shipped artifact, which every other gate in this repo reasons about only indirectly. semgrep, clang-tidy, CodeQL and CBMC all read source. The thing a user actually flashes is build/nrf5340dk/merged.hex, and between the source and that file sit a linker, a Kconfig tree, a generated device tree, a vendor blob and whateverwest builddecided to bake in. Nothing here has ever looked at the result. That gap is where a build-host path leak, a test key that survived a #ifdef, or a payload appended after the link would live, and none of those are visible to a source scanner by construction. scripts/security-fw.sh # every check, on the nRF5340DK image scripts/security-fw.sh --image out/x.bin # explicit artifact scripts/security-fw.sh strings # one: keys strings size dwarf make security-fw Exit 0 clean, 1 on a finding, 2 if there is no artifact to examine. Intel HEX is parsed here rather than shelled out to objcopy. objcopy is not on a mac by default and arm-none-eabi-objcopy lives inside the NCS toolchain, so requiring either turns "the gate ran" into "the gate ran if you had bootstrapped", which is the soft-skip this repo's gates are written to refuse. The parser below is thirty lines and has no dependencies. Env: FW_IMAGE=path artifact (default: the nRF5340DK image under $ALIRO_BUILD_ROOT) The size baseline is calibrated to THAT image, so pointing this at another board's build compares against the wrong record. FW_DENYLIST=path byte patterns that must not ship (default: security/fw-denylist.txt) FW_SIZE_BASELINE=path recorded sizes (default: security/fw-size-baseline.txt) FW_SIZE_WARN=2 FW_SIZE_FAIL=10 growth percentages FW_UPDATE_BASELINE=1 rewrite the size record instead of comparing NO_COLOR=1 plain outputsecurity-web.shsecurity-web.sh — the browser half of the supply chain, which nothing else in this repo looks at. scripts/security.sh'sdepsgate reads tools/tui/bun.lock and integration/homeassistant's pyproject. Neither of those is what a user actually executes. web-flasher/index.html executes a module fetched at page load from a CDN, and that page's whole purpose is to write firmware to a board over WebSerial — so whoever controls that module controls what gets flashed onto every device of everyone who used the hosted flasher. It is not in any lockfile, sodepshas never seen it; semgrep's p/javascript pack parses .js, not a <script> tag inside .html, so semgrep has never seen it either; and security-diff.sh's URL check only fires on a URL being ADDED, so a dependency that has been there since the page was written is invisible to all three. This gate closes that. It reads every tracked HTML page and asks three questions of it: 1. Is every remote subresource pinned to an exact version AND carrying an integrity hash? A range like@10resolves to whatever the registry serves at page load. That is not a dependency, it is a promise from a stranger, andintegrity=is the only thing that makes the difference observable to the browser. 2. Does the page carry a Content-Security-Policy? GitHub Pages sends no CSP header and cannot be made to, so a <meta http-equiv> is the only place one can exist for this project. Without it, an injected script has the same authority as the page: on the flasher, that is navigator.serial. 3. Is the vendored JavaScript free of known-vulnerable versions? (retire.js) scripts/security-web.sh # every check scripts/security-web.sh pins # one check: pins csp retire make security-web Exit 0 if everything selected passed, 1 otherwise, 2 on bad usage. Baseline, not suppression: security/web-baseline.txt lists paths that are knowingly non-compliant, one per line, each with a reason after a '#'. A baselined path still prints, it just does not block — so the debt is visible on every run rather than deleted. An entry that no longer matches anything is itself an error, because a stale baseline is how a check quietly stops applying to the file it was written for. Env: WEB_BASELINE=path override the baseline file NO_COLOR=1 plain outputsecurity-workspace.shNo module docstring. First commit: "security: add the eight-gate scanning lane".security.shsecurity.sh — the four fast security gates, in one place. CI (.github/workflows/ci.yml, via make verify),make securityand thesecrets/mal-diff/semgrep/depsrows in scripts/verify.sh all call THIS file. That is the point of it: the repo already learned once that a gate reproduced by hand in two places drifts in one of them, which is why verify.sh's header insists on running the same command CI runs. Here there is only one command. scripts/security.sh # all four gates, in order scripts/security.sh semgrep # one gate make security # same thing, through the front door Gates: secrets gitleaks over the tree, or over a commit range when one is given mal-diff scripts/security-diff.sh, the structural malicious-change checks semgrep security/*.yml plus the pinned registry packs; ERROR blocks, WARNING reports deps osv-scanner on the bun lockfile, pip-audit on the Home Assistant dependencies Exit 0 if every gate selected passed, 1 otherwise. A gate whose tool is missing FAILS rather than skipping, for the reason verify.sh gives at length: CI runs it whatever this host has, so "could not check" has to read as "not verified", never as "fine". Env: SECURITY_BASE / SECURITY_HEAD commit range; CI passes the PR's base and head SEMGREP_NO_REGISTRY=1 local rulesets only, skipping the network fetch NO_COLOR=1 plain outputspake2p_verifier.pyDerive a SPAKE2+ verifier (w0 and L) for a Matter setup passcode.test-runner.shPretty umbrella runner for every host-side suite: one banner, live per-check rows, a per-suite summary table, and suite timings. The suites themselves are unchanged — this only orchestrates and renders their existing output: firmware (C host) tests/host/run.sh the KAT suite + the lab python suite shared core (C host) ports/esp32/test/run.sh reader/stepup/crypto/... stages web twin scripts/twin-suite.sh constant-drift gate + WASM selftest Default: suites run in parallel, output replayed in order when done. SERIAL=1 streams them live, one at a time. SUITES="firmware shared" scopes. Exit is nonzero if any suite fails. Colour off when not a TTY or NO_COLOR.toolchain.shtoolchain.sh — what the CI gates need, whether this host has it, how to get it.make verifyruns eighteen CI gates and skips loudly when a gate's tool is absent. Skipping loudly is honest, but it leaves the reader to work out what to install, from where, and at which version. That is this script: one manifest, two modes. scripts/toolchain.sh report every tool, its gate, and its status scripts/toolchain.sh install install the missing ones, after confirming Nothing is installed without being printed first and agreed to.installshows the exact command list and waits for a y; -y answers it in advance for unattended use. Versions matter for four of these. clang-format and clang-tidy change their output between releases, so a host one version off the CI pin fails a gate that CI passes (or worse, the reverse). Those rows carry the pin CI uses and say so when the host disagrees. Out of scope, same boundary as verify.sh: the firmware toolchains. NCS (~6.5 GB,make bootstrap) and ESP-IDF are per-target installs with their own documented procedures — see docs/set-up.md. This covers the host gates only. Adding a gate to verify.sh without adding its tool here is caught:checkreads verify.sh's own gate_need + gate_need_py tables and fails on any name it cannot explain, and fails again if either table stops parsing. What it does NOT catch is a row here that no gate needs any more, and none of it runs in CI — only when someone runsmake tools. Env: ASSUME_YES=1 same asinstall -yNO_COLOR=1 plain outputtwin-suite.shThe web-twin suite for the umbrella runner (make check): the constant-drift gate (always) plus the WASM twin's node self-test against the committed web-twin/twin.js (when node is present). No rebuild here — regenerating twin.js needs a pinned emsdk and is CI's byte-diff staleness gate; this only proves the committed firmware artifact still passes its scenario.twin-wasm.shBuild the web twin's firmware: modules/woz_uwb + the tests/host shim compiled to WASM (Emscripten), driven by web-twin/twin_glue.c. Output is a single self-contained web-twin/twin.js (MODULARIZE + SINGLE_FILE: the .wasm rides embedded, so the page keeps working from file:// and the site copy stays a flat file pair). The compile is path-prefix-mapped for reproducibility: the same emsdk version must produce a byte-identical twin.js on any machine, which is what lets CI rebuild and diff it as a staleness gate.verify.shPre-push sweep: every CI gate that a host can run, in one shot. The point of this script is that "it passed locally" and "it will pass CI" mean the same thing. Each row below is one CI *job* (not one workflow — one job in ci.yml now runs all of them), running the same command that job runs. Adding a job to .github/workflows/ without adding it here re-opens the gap this script exists to close. Out of scope, deliberately: firmware-builds.yml and release.yml. They need ESP-IDF and NCS (~6.5 GB of toolchain) and take tens of minutes — not a push gate.make buildcovers them once the toolchain is bootstrapped. The gates run in lanes, several at once, because serially they are ~83s of work on a machine with eight cores. A short serial tripwire goes first, so a formatting slip still stops the sweep about four seconds in; then the expensive gates run together and the sweep costs its slowest lane rather than the sum of all of them. Measured back to back on an idle host: 83s serial, 34s in lanes, and 72s in lanes with cbmc on against 147s serial. SERIAL=1 puts it back to one gate at a time, for a busy machine or for reading a confusing failure in order. One gate does not run by default: cbmc. At 64s it is twice the rest of the sweep put together, spent on the gate whose input moves least — the wire parsers it proves have been stable for months, and the fuzz gate exercises the same code every run. WITH_CBMC=1 turns it on, taking the sweep to ~72s. It still gets a summary row saying it did not run: a gate that quietly disappears from the sweep is the exact failure this script exists to prevent. The PR runs it whenever the branch touches what it proves — which since the path filter below is a narrower claim than this comment used to make, and the reason WITH_CBMC=1 in CI is no longer the same as "on every pull request". A gate whose tool is missing FAILS the sweep. It says so on its row, it is counted apart from a hand-scoped SKIP=, and the run exits nonzero. Anything softer is the original bug wearing a warning label: CI runs that gate whatever this host has installed, so "could not check" has to read as "not verified", not as "fine".make tools-installis the fix; SKIP="<gate>" is the override for someone who has decided to accept the gap. Most gates only read part of the tree, so most changes cannot break most of them. A gate whose inputs this branch does not touch is skipped with a row saying so — see the path-filter section below for how that is decided, and for the four conditions that turn the whole thing off and sweep everything. Env: WITH_CBMC=1 also run the cbmc proof (off by default, see above) SERIAL=1 one gate at a time, fail-fast, instead of lanes SKIP="cbmc fuzz" space-separated gate names to leave out of this run FILTER=0 run every gate whatever changed, ignoring the path filter FILTER_BASE=<ref> what "changed" is measured against. Unset means origin/main; set-but-empty means there is no base, and the filter is off. COV_MIN=90 line-coverage floor. Reported, never blocking: under it the row still passes and says so. Raise it to aim higher. NO_COLOR=1 plain output (colour is the default, pipe or not) FAIL_TAIL=40 lines of a failing gate's log to show inlinewoz_patch.pyBuild a signed delta patch for the DWM3001CDK's over-the-air update path.woz_push.pyPush a signed delta patch to a DWM3001CDK over Bluetooth.woz_smp.pyPush a delta patch to the board over SMP, the way a phone would.ws-seed.shws-seed.sh — give this git worktree its own NCS workspace, cheaply. Frequent branch-bouncing over a single shared workspace is a trap: the tree holds one patch state at a time (last bootstrap wins), so a build from the wrong worktree silently compiles another branch's patches. This seeds a per-worktree workspace at the default path ($TREE/workspace) so build.sh picks it up with no env var, and each worktree stays self-contained. Cheap because it uses an APFS copy-on-write clone (cp -c): the clone shares every block with the primary and costs ~0 extra disk until a patched file diverges. Cleanup is automatic — the workspace lives inside the worktree, so deleting the worktree deletes it (seemake ws-clean).
toolstools38 modules
main.tsxaliro_gait.pyAliro Gait: carry-motion features from Aliro Lab walk-up captures.app.tsxserial.tsaliro_lab.pyAliro Lab: score a captured reader serial log.devices.tsjobs.tsmotion.tsNo module docstring. First commit: "Give the bench TUI its labels back as border rules".search.tsSearching the serial scrollback. Pure string work, kept out of app.tsx so it can be tested directly instead of through a rendered terminal. Matching is case-insensitive and literal: a firmware log is full of[,*,0x..and?, so treating the query as a regular expression would turn ordinary searches into syntax errors.targets.tsterminal.tstheme.tstypes.tswizard.tspresence_git.pyPresence-signed git tags: prove a human was physically present at a release.presence_verify.pyVerify an ECDSA-P256 presence assertion against a dongle's public key.aliro.luaNo module docstring. First commit: "Add Wireshark dissector for the clear-text Aliro BLE plane".aliro_blob.pyInspect an aliro_prov ("APRV") reader-provisioning blob.docs_3d.pyRender the whole code surface as a flyable 3D graph: site/graph3d.html.docs_api.pyFill the reference pages the page generator leaves bare.docs_apilinks.pyPoint each narrative page at the declarations it is describing.docs_cmds.pyRender runnable command blocks as one copy chip per command.docs_flash.pyPublish the browser flasher: site/flash/ = the web-flasher/ page + firmware.docs_github.pyPoint the rendered site back at its GitHub repository.docs_graph.pyMake the architecture page's dependency graph legible.docs_hero.pyStage the site: a cinematic landing hero, and a reveal layer everywhere.docs_links.pyRepair cross-document links in the rendered site, then assert none are left broken.docs_media.pyAdd the repo's imagery to the rendered site: demo screenshots and a share card.docs_modules.pyMove the per-file reference listing off the landing page onto its own.docs_motion.pyThe motion layer: choreograph the arrival, and make the page answer back.docs_nav.pyGive the rendered site one curated reading order.docs_start.pyGive the rendered site a real "Get started" landing.docs_theme.pyRetheme the rendered site: warm paper surfaces, serif display headings.docs_title.pyTitle the generated pages after the repository, not after the checkout directory.docs_twin.pyFold the interactive walk-up digital twin into the rendered site.flight_recorder.pyflight_recorder.py — carry a recorded UWB walk-up off the device and turn it into replayable / fuzzable artifacts.piv_pin.pyProvision or change the OpenAliro PIV PIN through macOS PC/SC.power_profile.pyPower profile: turn a gated-walk-up serial log (+ optional power capture) into the mA / unlock-latency / approach numbers of the RSSI-gate study.
web-twinweb-twin3 modules
check_constants.pyVerify that the web-twin's hardcoded firmware constants in index.html stay synchronized with their source definitions. Parses the FW table, reads the cited source lines, and reports any mismatches or missing citations.csp_probe.pyPhase 0 spike, local half.twin_glue.cWASM entry points: the twin page's firmware harness. Compiled (emcc) with the untouched modules/woz_uwb sources plus the same tests/host shim the host suite links, so the page runs the real responder: every block is a genuinely CCM*-encrypted Pre-POLL/POLL/Response/Final/ Final_Data exchange decoded by the firmware's own RX state machine, and the page reads its decisions through the same facade seam the lock uses. The peer (iPhone) side comes from tests/host/twin_frames.c — shared with test_twin.c, so the page and the suite drive the responder identically. The JS above supplies only the world: target distance, noise, spoof timing, and the pacing of the five per-block legs (PREPOLL/POLL/TXDONE/FINAL/FINAL_DATA) so a visitor can single-step a live DS-TWR round. Distance is injected the way physics does it: the initiator-side DS-TWR intervals ride in the Final_Data as round1 = reply1 + 2*tof and reply2 = round2 - 2*tof, which makes the firmware's own (round1*round2 - reply1*reply2)/sum recover exactly tof ticks (1 tick ~ 15.65 ps, ~4.692 mm — ccc_shim_rx.c final_data_decode). A Ghost-Peak spoof is a negative-tof block through the same full path.
woz_aliromodules/woz_aliro37 modules
aliro_ranging.cUWB ranging bring-up and lifecycle for the Aliro reader: initializes the reader's UWB adapter and Cherry CCC context once, then arms, feeds, and tears down per-connection ranging sessions driven by the M1-M4 setup exchanged over the peer's L2CAP channel. Maintains process-wide singletons for the Cherry context and adapter (set up once via aliro_ranging_init) and for the single active ranging session (the DW3000 supports only one session at a time), tracking its owning secure channel for send/receive framing.aliro_reader.cAliro reader engine: drives the Access Protocol (AUTH0/AUTH1/EXCHANGE) handshake over BLE, manages reader identity and credential trust provisioning in NVS, and arms UWB ranging once a session is authenticated. Maintains a fixed-size table of per-connection sessions tracking transaction phase and secure-channel state, and exposes start/attach entry points for both standalone and Matter-attached BLE transports, plus provisioning and diagnostic APIs used by Matter commissioning and the bench console.aliro_device.cAliro initiator (User-Device) session machine: the implementation behind aliro_device.h. Feeds one reader command at a time through aliro_device_on_command, which parses AUTH0/AUTH1/EXCHANGE with the inverse codec, runs the mirror of the reader's key schedule (ephemeral ECDH, the two ECDSA transcripts, the session salt) and returns the sealed response. Owns the two AES-256-GCM channels the device holds, the Access-Protocol channel and the BleSK ranging channel, both split out of the same 160-byte key block, plus the standard-path derivation factored EC-free so host tests can drive it with a supplied shared secret.aliro_lat.cWalk-up latency trace: first-hit phase timestamps + the consolidated budget line.aliro_assert_ec.cBinds the aliro_assert P-256 seam to aliro_prim's ECDSA (see aliro_assert_ec.h). The only file in the presence path with a crypto-backend dependency, which is exactly why it is separate: aliro_assert.c keeps its cbmc and fuzz harnesses.aliro_crypto.cAliro cryptographic primitives: key derivation (KDF/HKDF), key-block splitting, AES-GCM secure channels, and wire message framing built on a pluggable crypto backend (aliro_prim_*). Implements the Aliro key-derivation chain (ECDH shared secret -> z -> 160-byte key block -> split session keys / URSK / BLE ranging keys), per-direction AES-256-GCM secure channels with monotonic message counters, and the seal/open framing used to carry engine plaintext over the wire.aliro_stepup.cAliro step-up phase codec + verifier: derives the StepUpSK SessionData keys, builds the mdoc DeviceRequest and its ENVELOPE/GET RESPONSE APDUs, seals/opens SessionData over the aliro_secchan AES-256-GCM channel, and runs the six-step Access Document verification of spec 7.4. The ES256 primitive is injected (verify ctx) so this unit carries no elliptic-curve dependency.aliro_advtag.cAliro BLE advertisement Dynamic Tag derivation (Aliro 1.0 section 11.3.1), shared by the BLE transport (live advertising) and the host KAT suite (spec section 20 worked examples).aliro_assert.cPresence-assertion wire codec + verifier (see aliro_assert.h). Serialises a dongle's "credential present within N cm for this nonce" statement and verifies an ECDSA-P256 frame against a challenge nonce, enrolled credential and distance threshold. Portable C11; no UWB/BLE/platform dependencies.aliro_ble_central.cPlatform-free half of the device-side BLE transport declared in aliro_ble_central.h: decodes the reader's 0xFFF2 service-data advert, decodes the reader-SPSM GATT READ payload (SPSM, supported protocol versions, feature mask), and assembles the BleSK salt from the version list the reader actually published rather than from a compiled-in constant. No BLE stack calls and no allocation, so it builds on the host and is checked byte for byte against the reader's own emitters.aliro_device_apdu.cImplementation of the device-side Access-Protocol wire codec declared in aliro_device_apdu.h: ISO7816 case-4 unwrapping, status-word appending, parsers for the reader's AUTH0, AUTH1 and EXCHANGE command TLVs, and builders for the three device responses. Every function is bounds-checked byte manipulation over caller-owned buffers with no allocation, so it round-trips against the reader's own builders and parsers in aliro_apdu.c under the host tests.aliro_stepup_parse.cDeviceResponse structural decoder for the Aliro step-up phase: a minimal, bounds-checked, depth-limited CBOR reader (definite-length core-deterministic only) plus the Table 8-22/7-1/7-2 field walk. No crypto and no allocation; every parsed field is a slice of the caller's buffer. This is the wire-facing attack surface and is fuzzed on its own (tests/host/fuzz/fuzz_stepup.c).aliro_apdu.cAliro APDU TLV codec: builds command payloads (AUTH0, AUTH1, AuthData, EXCHANGE) and parses response APDUs, plus BLE envelope framing/unframing and ISO7816 APDU wrap/status-word stripping. Provides a minimal BER-TLV writer (aliro_tlv_w_init/put/finish) used to assemble command payloads, and TLV/APDU parsing helpers used to extract fields from device responses.aliro_approach.c@file aliro_approach.c Kalman-filtered approach controller for predictive unlock. Tracks distance (cm), velocity (cm/s), and estimated time-to-arrival (ms) at the unlock radius. Supervises presence via median filtering of trusted ranges and fires predictive unlock when closing speed and ETA meet thresholds. Factory defaults: unlock 100 cm, relock 250 cm, dwell times 2 s and 3 s, motor delay 500 ms, margin 250 ms, velocity floor 30 cm/s, prediction enabled.aliro_hash.cSelf-contained SHA-256, HMAC-SHA256, HKDF, and ANSI-X9.63 KDF implementation for the ESP32-IDF Aliro crypto port, with no external crypto library dependency.aliro_prim_psa.cAliro crypto primitive backend implemented on Arm PSA Crypto: random generation, AES-256-GCM encrypt/decrypt, and NIST P-256 key generation, ECDH, and ECDSA sign/verify. Provides the aliro_prim_* / aliro_* primitive functions consumed by the higher-level Aliro KDF and secure-channel code in aliro_crypto.c; callers must call aliro_prim_init before using any other function in this file.aliro_prov.cAliro reader provisioning state: default dev identity, and serialization/deserialization of the reader identity plus trusted-credential store to/from a self-describing binary blob. Also implements the trust-store membership check and add-with-dedup operations used to decide whether a presented credential public key is trusted.aliro_rssi_gate.cBLE-RSSI ranging power gate implementation: EWMA smoothing in Q4 fixed point, open/close hysteresis with a sustained-below close hold, and an optional rise-rate fast open so a fast approach is not penalized by the smoothing lag. Pure logic — no radio, clock, or logging dependencies — so the host suite can drive it with synthetic approach traces.aliro_ble.hAliro BLE-UWB reader transport: GATT service definition, advertised feature flags, and transport callbacks connecting the BLE peripheral role to the Aliro protocol handler in aliro_reader. Callers configure the transport via aliro_ble_prepare (which builds the READ characteristic payload without touching NimBLE), then register the GATT service returned by aliro_ble_service_def with the host's combined service table.aliro_crypto.hAliro crypto public API: key derivation, AES-GCM secure channels, and wire message seal/open framing shared by the reader and device sides of an Aliro session.aliro_lab.hAliro Lab trace: structured "[ALAB]" lines at transaction phase boundaries, parsed by tools/aliro_lab.py into a scored walk-up report. Ships in every Aliro build (CONFIG_WOZ_ALIRO_LAB defaults y, like the sibling uwbdiag trace) but is OFF at boot and toggled at runtime by thelab on/lab offconsole command, so any firmware profiles on demand with no reflash. Set CONFIG_WOZ_ALIRO_LAB=n to strip it from a hardened production image.aliro_lat.h@file aliro_lat.h Latency tracking for Aliro protocol phases during a walk-up: record BLE_CONNECT as epoch zero, mark timestamps for each phase, emit a report with elapsed intervals and flight-recorder diagnostics.aliro_ranging.hAliro M1-M4 ranging-setup interface: negotiates UWB ranging parameters with the device and produces the BLE ranging-control secure channel used to carry the M1-M4 exchange.aliro_prim.haliro_prov.hPersistent reader provisioning storage: identity and credential trust anchors saved to and loaded from NVS. Declares aliro_prov_store for committing an identity/trust pair to NVS, and struct aliro_trust_store, the set of trusted credential public keys against which a presented credential is authenticated.aliro_reader.haliro_rssi_gate.hBLE-RSSI ranging power gate: decides when the phone is close enough that arming UWB ranging is worth the radio's RX power. Pure sample-in/state-out logic (EWMA smoothing, open/close hysteresis with a close hold-off, optional rise-rate fast open for fast approaches) so it host-tests without a radio; the reader feeds it connection RSSI samples and defers Reader-Status-AP-Completed until it opens.aliro_stepup.hAliro step-up (Access Document) phase: builds the mdoc DeviceRequest, unwraps and decrypts the SessionData DeviceResponse, decodes the CBOR document per spec 7.2/8.4.2, and runs the six-step Access Document verification of spec 7.4. Reference-completeness codec + verifier; the verdict is logged and stored, never gates the unlock (the provisioned trust store remains the sole gate).aliro_apdu.hAPDU framing and parsing for the Aliro Access Protocol: builds outbound command APDUs via a TLV writer and parses the AUTH0/AUTH1 response APDUs exchanged during the reader-device handshake.aliro_hash.hStreaming SHA-256 (FIPS 180-4) implementation used by the Aliro crypto layer. Declares struct aliro_sha256, the incremental hash context used across init/update/finish calls.aliro_device.hAliro initiator (User-Device) session layer: the device-side counterpart of aliro_reader.c. Drives the credential-auth handshake from the phone/fob role — parses the reader's AUTH0/AUTH1/EXCHANGE commands, runs the mirror-image key schedule (ECDH, the two ECDSA transcripts, the §8.3.1.13 salt), and produces the sealed responses. The result is the same 32-byte URSK the reader derives.aliro_assert_ec.hNo module docstring. First commit: "assert: bind the P-256 seam to aliro_prim".aliro_advtag.hAliro BLE advertisement Dynamic Tag derivation (Aliro 1.0 section 11.3.1): the 7-byte GroupResolvingKey-resolvable tag the phone recomputes to identify a reader of interest.aliro_assert.hNo module docstring. First commit: "aliro: presence-assertion protocol (HMAC-signed range statement)".aliro_ble_central.hDevice-side (User-Device) BLE transport interface: the central/client mirror of aliro_ble.h. Where the reader advertises 0xFFF2, serves the GATT characteristics and runs an L2CAP CoC server, the initiator scans, connects, reads the reader's SPSM/versions, writes its selected version and opens a CoC client to that SPSM. The platform-free half (advert + READ-payload decoding, BleSK salt assembly) lives in aliro_ble_central.c and is host-testable; the NimBLE backend for the transport calls sits in ports/esp32, so a Zephyr bt_gap_*/bt_l2cap_* backend can be written behind this same header.aliro_device_apdu.hDevice (User-Device) side of the Aliro Access-Protocol wire codec: the inverse of aliro_apdu.c. Where aliro_apdu builds reader commands and parses device responses, this parses the reader's AUTH0/AUTH1/EXCHANGE commands and builds the device's AUTH0/AUTH1/EXCHANGE responses. Pure byte manipulation, no crypto and no platform dependency, so it is host-KAT verifiable against the reader's own builders/parsers (round-trip) and the recovered layouts.aliro_approach.h@file aliro_approach.h Configuration and state for approach detection and predictive unlock: unlock/relock thresholds in centimeters, sample-count dwell times, motor retraction time, scheduling margin, minimum closing speed, and a flag to enable or disable predictive ToA unlock.
woz_aliro_ecpmodules/woz_aliro_ecp/src1 module
woz_aliro_stackmodules/woz_aliro_stack/src18 modules
session.cpp@file session.cpp Aliro reader BLE session state machine and cryptographic session context. Manages NFC APDU limits, response timeouts, connection setup, fast-path and standard key derivation, message encryption and decryption, and reader-status notifications. Processes events from the BLE transport and application layer.ble_message.c@file ble_message.c BLE protocol message framing: parse and build protocol/message_id headers and payloads; parse and extract Initiate Access, UWB control, Access Completed, and Reader Status Changed messages.ble_timeout.c@file ble_timeout.c Aliro BLE timeout supervisor (state machine + reply validator). Core: classify_attribute parses BLE message type from attribute ID/length; is_allowed_reply maps request→reply types (including Busy/GeneralError for any); has_response_timeout marks messages that start a timeout window; collision_replaces_pending resolves priority when incoming messages arrive before the previous one completes; set_pending / clear_pending manage state transitions. Designed to prevent timeouts when the phone is responsive and terminate when not.nfc_auth.c@file nfc_auth.c NFC Aliro protocol command builders: AUTH0 and AUTH1 APDU encoding, authentication data construction, and response parsing for credential exchange and signature verification over NFC.nfc_select.c@file nfc_select.c NFC SELECT command builder and response parser for Aliro. build_select_command emits 00 A4 04 00 09AID00. parse_proprietary_information decodes type-0x80 data from a SELECT response, extracting protocol version (expedited phase only) and extended-length sizes (0x7f66 TLV). parse_select_response and parse_select_response_ex validate the trailing 9000, check AID, and call parse_proprietary_information.nfc_step_up.c@file nfc_step_up.c NFC step-up messaging: compact-key CBOR encoder/decoder for Aliro DeviceRequest and SessionData (ISO 18013-5). Core: put appends to writer buffer; cbor_head / cbor_bytes / text build encoded items; cbor_read_head parses with validation (non-minimal representation rejected); build_device_request constructs DeviceRequest (compact keys); wrap_session_data / unwrap_session_data encode/decode SessionData; wrap_do53 / unwrap_do53 TLV-wrap messages; build_envelope_command / build_get_response_command and collect_response chain ISO APDU commands.advertising_core.c@file advertising_core.c Compute dynamic advertisement tag inputs and extract tags from AES ciphertext. The plaintext input incorporates the device's BLE address and an expiry timestamp; the tag is derived by AES encryption and truncation for inclusion in Aliro BLE advertisements per specification section 20.aliro_stack.cppIndependent implementation of the Nordic Aliro public API used by this app. Protocol constants and wire formats come from Aliro Specification 1.0.access_document.c@file access_document.c Compact-key CBOR parser for Aliro Access Documents (compact subset of ISO 18013-5 mDoc). Parses strictly with iterative depth traversal (no stack recursion), validates CBOR encoding (no floats, no simple values with payloads, minimal representation), and enforces a 25-level nesting bound. Core: parse_at walks encoded items; root validates full-buffer consumption; child_at / map_find_* retrieve nested elements; integer / timestamp extract scalar fields.tlv.c@file tlv.c BER-TLV parser and encoder for Aliro protocol: parse TLVs with definite length and advance offset, compute encoded sizes, and write new TLVs.access_document.h@file access_document.h Aliro access document parsed from CBOR and COSE_Sign1 envelope: device public key, issued data element, issuer-signed item, signature, issuer key ID and certificate, validity period, and optional iteration count.ble_message.hAliro 1.0 Bluetooth LE message framing (section 11.7).ble_timeout.hAliro 1.0 Bluetooth LE responseTimeout rules (section 11.9).nfc_auth.hAliro 1.0 expedited authentication APDU codecs.nfc_select.h@file nfc_select.h Parsed result of an NFC SELECT command for the Aliro applet: negotiated protocol version, maximum command and response data lengths (from TLV or default), extended-length support, and the raw proprietary information TLV (A5 tag) for further parsing.nfc_step_up.hAliro 1.0 / ISO 18013-5 NFC step-up message and APDU codecs.tlv.hMinimal strict BER/DER-TLV reader for Aliro APDU payloads.advertising_core.hAliro BLE advertising primitives. Kept as portable C so the byte-order rules can be tested on the host using the specification's published known-answer vectors.
woz_dfumodules/woz_dfu6 modules
dfu_receiver.cApplication half of the delta update: receive, verify, stage, reboot. Never applies anything. The patch is written intopatch_stagingand the board is restarted; MCUboot does the work, because the application executes from the slot the patch rewrites (see src/dfu_applier.c). WHAT ARRIVES, in order, as one byte stream over whatever transport: 0 32 struct woz_dfu_hdr 32 64 ECDSA-P256 signature, raw r||s, over those 32 bytes 96 .. the patch The header is written to flash LAST, after the whole patch has arrived and its CRC has been checked. So a transfer that is cut off leaves a staging partition with no valid magic in it, and the next boot ignores it. There is no half-staged state that the bootloader can act on. THE SIGNATURE IS CHECKED HERE, NOT IN THE BOOTLOADER. This image already has PSA ECDSA-P256 linked for Aliro; MCUboot is the flash-starved one. And the floor sits under both: CONFIG_BOOT_VALIDATE_SLOT0 makes MCUboot re-verify the P-256 signature of the RESULT before booting it, so even a forged header cannot install code -- only destroy the installed image, which recovery catches.dfu_smp_img.cSMP image-management group, so a stock mcumgr client can push a delta. WHY THIS EXISTS INSTEAD OF ZEPHYR'S img_mgmt. Zephyr's implementation cannot be built here, and not for a reason a partition rename fixes: CONFIG_MCUMGR_GRP_IMG ... unsatisfied dependencies: IMG_MANAGER (=n), (!MCUBOOT_BOOTLOADER_MODE_SINGLE_APP) (=n) img_mgmt is gated OFF by single-slot mode itself. That mode is not incidental on this board -- it is the only reason MCUboot fits, because two slots want 844 KB of a 512 KB part (firmware/pm_static.yml does the arithmetic). So the choice was to leave single-slot mode, which the flash forbids, or to serve group 1 ourselves. This is the second. It is a thin adapter, not a reimplementation: every byte still goes through woz_dfu_rx_upload(), so the signature check, the size limits, the CRC and the window gate are the same ones the native transport uses, in the same order. What is new here is only CBOR in and CBOR out. WHAT A CLIENT SEES. One image, one slot, active and confirmed, versioned and hashed from the running MCUboot header. Uploads are accepted and staged. It does NOT pretend to have a second slot, because there is no honest hash to report for one -- the staged bytes are a patch, and what they produce is not known until the bootloader has applied it. SO THE GUIDED "FIRMWARE UPGRADE" WIZARD IS NOT THE TARGET. That flow wants upload -> test -> reset -> reconnect -> confirm, and two things break it: the device never reports a pending second image to confirm, and the reboot after reset spends 17-31 s applying the patch, which outlasts the client's reconnect window. The supported path is the plain one, and it is three taps: 1. Images -> Upload, choose the .woz patch (this file, group 1 cmd 1) 2. Device -> Reset (os_mgmt, group 0 cmd 5) 3. wait ~30 s while MCUboot applies it (src/dfu_applier.c)dfu_applier.cApplies a staged delta patch onto the primary slot, from inside MCUboot. Runs as a SYS_INIT at APPLICATION level. That level is chosen, not convenient: it is after the flash driver has initialised (POST_KERNEL) and before MCUboot's own main(), which is the only window in which the primary slot can be rewritten. It also means NOT ONE LINE of fetched upstream MCUboot is edited -- the bootloader loads this the same way it loads every other Zephyr module. Why this cannot live in the application: the application executes from the primary slot. Rewriting it would be rewriting the code doing the rewriting. On a normal boot this costs one word read: the header magic does not match and the function returns immediately. SAFETY. Three things stand between a bad patch and a dead lock, and only the third is load-bearing: 1. the header carries a CRC of itself, written last, so a torn write fails 2. the patch and the from-image are CRC-checked before a byte is erased 3. MCUboot re-verifies the P-256 signature of the RESULT before booting it (CONFIG_BOOT_VALIDATE_SLOT0=y), and drops to serial recovery if it fails (CONFIG_BOOT_SERIAL_NO_APPLICATION=y) So the worst a corrupt or forged patch achieves is destroying the installed image, which is recoverable, rather than installing code, which is not. POWER CUTS ARE EXPECTED, not exceptional: this rewrites most of 442 KB and takes seconds. detools' step counter is what makes that survivable -- see step_set()/step_get() below.woz_dfu.hThe on-flash contract between the application and the bootloader for a delta firmware update. The application receives a patch over Bluetooth and writes it into thepatch_stagingpartition. MCUboot reads it on the next boot and applies it onto the primary slot. Nothing else connects the two, so this header IS the interface: a change here that is not made on both sides produces a board that stages an update and then silently declines to install it. Plain C11 with no Zephyr dependency, so the host tests and the patch builder can include it and agree on the layout by construction rather than by transcription.woz_dfu_rx.hReceives a delta patch into the staging partition, application side. Transport-independent on purpose. The DWM3001CDK feeds this from a second L2CAP CoC beside the Aliro one, but nothing here knows that -- it takes frames and returns replies, so the host tests can drive it without a radio. The bootloader half is @ref woz_dfu.h. This side never applies anything: it writes bytes, checks a signature, and reboots.emit_pubkey.pyEmit the public half of the MCUboot signing key as a C array.
woz_mattermodules/woz_matter30 modules
matter_case.cNo module docstring. First commit: "woz_matter: CASE, the responder's first half".matter_clusters.cNo module docstring. First commit: "woz_matter: the Interaction Model, as far as a commissioner needs it".matter_pase_sm.cPASE responder state machine. See matter_pase_sm.h.matter_exchange.cthe unsecured exchange. See matter_exchange.h.matter_crypto.cAES-128-CCM, the Matter nonce, and the key schedule.matter_fabric.cNo module docstring. First commit: "woz_matter: AddNOC, accepted by a real iPhone".matter_attest.cNo module docstring. First commit: "woz_matter: attestation, accepted by a real iPhone".matter_im.cNo module docstring. First commit: "woz_matter: the Interaction Model, as far as a commissioner needs it".matter_pase.cPASE message codec over Matter TLV.matter_spake2p.cPBKDF2, the SPAKE2+ transcript and confirmations.matter_btp.cBTP handshake codec, fragmenter and reassembler.matter_mrp.cMRP backoff schedule, retransmit state, replay window.matter_msg.cMatter message and protocol header codec.matter_tlv.cMatter TLV codec, encoder then decoder. Control byte = tag control (top 3 bits) | element type (bottom 5). Then the tag octets, then the value. Everything multi-octet is little-endian.matter_case.hproving an operational identity, both ways. PASE let a commissioner in because it knew a printed code. CASE is what happens afterwards, every time: two nodes that already hold certificates from the same fabric prove it to each other and agree on session keys. It is the only session type the spec will accept CommissioningComplete over, and the only way a phone talks to this node once BLE is gone. Sigma1 initiator -> responder who I want, and my ephemeral key Sigma2 responder -> initiator my certificate chain, signed, encrypted Sigma3 initiator -> responder the same, in the other direction This file is the responder's half, built in that order. The subtle piece is Sigma1's destinationId. It is not an address: it is an HMAC that only somebody holding the fabric's identity protection key could have produced, over the identity they are asking for. A responder does not read a node id out of it -- it recomputes the HMAC for each fabric it holds and looks for a match. That is what makes an unsolicited Sigma1 unable to enumerate a node's fabrics: get the key wrong and you learn nothing.matter_crypto.hMatter message crypto: nonce, AES-CCM, session keys. Matter secures every message with AES-128-CCM: a 13-byte nonce built from fields the peer can see, a 16-byte tag, and the plaintext message header as additional authenticated data so the routing fields cannot be edited in flight. nonce security_flags:u8 message_counter:u32 node_id:u64 (little-endian) aad the message header exactly as it appears on the wire keys HKDF-SHA256(secret, salt, "SessionKeys") -> i2r | r2i | challengematter_fabric.hthe operational identity a commissioner installs. Attestation ends with the commissioner holding a public key this node proved it owns. What follows is the commissioner handing back an identity built on that key: AddTrustedRootCertificate trust this root AddNOC and here is who you are underneath it Both certificates arrive as MATTER TLV, not X.509. The spec defines a compressed form precisely so a constrained node can read one without an ASN.1 decoder, and this file is that reader. It reads three things and ignores the rest: the subject's node id, its fabric id, and the public key. Validity dates, key usage and the signature are what a node checks when VERIFYING a certificate somebody else presents, which is CASE's job. A commissioner has no reason to lie to itself about a NOC it just minted, and this node cannot check the signature anyway without the issuer's key -- which, for the NOC, is the root it was told to trust one command earlier and has no independent reason to believe.matter_im.hthe Interaction Model, as far as a commissioner needs it. Once PASE is done the commissioner stops speaking Secure Channel and starts asking questions. The first one is a ReadRequest, and until something answers it the phone waits, gives up, and shows "pairing failed" with no further clue. in ReadRequest (protocol 0x0001, opcode 0x02) out ReportData (protocol 0x0001, opcode 0x05) This is the read half only. Write, Invoke and Subscribe are separate messages and land when something needs them; commissioning cannot start without Read. It holds no device data. Which endpoints exist and what their attributes say is matter_clusters.h's, reached through @ref matter_im_server, so the wire format can be tested without a device and the device without a wire.matter_tlv.hMatter TLV codec (Matter Core spec, Appendix A). This is NOT the BER/DER-TLV in modules/woz_aliro_stack/src/protocol/tlv.h. Matter uses its own encoding: one control byte carrying a 3-bit tag control and a 5-bit element type, then 0-8 tag octets, then the value, all little-endian. The two share a name and nothing else, so they stay separate.matter_clusters.hwhat this device answers, as opposed to how. matter_im.c owns the ReportData wire format and knows nothing about door locks or vendor IDs. This is the other half: the endpoints, clusters and attributes that exist, and what they say. Scope is deliberately the commissioner's FIRST question and no further. A real iPhone, immediately after PASE, reads nine attribute paths: endpoint 0 GeneralCommissioning 0x0030 attributes 0x00..0x04 and 0x0C endpoint 0 BasicInformation 0x0028 VendorID 0x02, ProductID 0x04 endpoint 0 TimeSynchronization 0x0038 all attributes (wildcard) Everything else answers UNSUPPORTED_*, which is a legal answer and a truthful one. Clusters get added when a commissioner is observed asking for them, rather than because the spec lists them. Device-specific values arrive in @ref matter_device_info instead of being read from Kconfig here, so the host suite can build this without Zephyr and assert on the encoded bytes.matter_pase_sm.hPASE responder: the device side of the five messages. matter_pase.h is the codec and matter_spake2p.h is the arithmetic; this is what drives them. A commissioner opens with PBKDFParamRequest and this answers, receives Pake1, answers Pake2, receives Pake3, and ends with a StatusReport. What comes out the far side is a session key schedule. -> PBKDFParamRequest <- PBKDFParamResponse (context hash fixed here) -> Pake1 (pA) <- Pake2 (pB, cB) -> Pake3 (cA) <- StatusReport(success) The device never holds the setup passcode. It holds the SPAKE2+ verifier -- w0 and L -- which is derived from the passcode somewhere else and provisioned in. That is the whole point of the augmented form: someone who reads the device's flash cannot impersonate a commissioner to it. No time and no randomness are taken from the environment. Retransmission is MRP's job (matter_mrp.h), and the two random values PASE needs are arguments, so the host suite runs the real state machine against a recorded exchange rather than against whatever entropy it happened to get.matter_exchange.hthe unsecured exchange PASE runs on. Between BTP (a byte pipe) and PASE (five messages) sits the part that makes a Matter message a message: which session it belongs to, which exchange, whether it is a duplicate, and whether the peer is owed an acknowledgement. in message header | protocol header | payload out message header | protocol header | payload This handles exactly one exchange on the UNSECURED session, which is all commissioning needs before PASE finishes: session id 0, no encryption, the peer as initiator and this node as responder. Secure sessions are a different object -- they carry keys and a different counter -- and arrive with CASE. It deliberately does not know what PASE is. It reports the opcode and hands back the payload; the caller decides what to answer. That keeps the framing testable on its own, and means CASE will reuse it rather than fork it. No timers here either. Duplicate suppression and the ack bookkeeping are state, not scheduling; retransmission is matter_mrp.h's, driven by whoever owns a clock.matter_attest.hproving to a commissioner that this is a real device. After the fail-safe is armed, the commissioner stops asking what this node is and starts asking it to prove it. Three questions, in this order: CertificateChainRequest give me your DAC, then your PAI AttestationRequest sign this nonce with the DAC's private key CSRRequest make me a key I can certify, and sign for it The certificates are static blobs. The signatures are not: each covers the message AND the session's attestation challenge, which is why a recorded exchange cannot be replayed into a different session. WHAT THESE CREDENTIALS ARE. The DAC, PAI and CD here are the SDK's published development credentials for vendor 0xFFF1 / product 0x8001, and the DAC's private key is published alongside them. They prove nothing about who built this device -- anyone can extract the same key from a public repository, and a commissioner that enforces attestation will reject them. They are here so commissioning can be developed against a real phone; shipping a product means a DAC issued under a real PAI, and its private key must not live in flash next to the certificate.matter_pase.hPASE message codec (the five commissioning messages). PASE is how a commissioner proves it knows the setup passcode. Five messages, all Matter TLV structures on the Secure Channel protocol: PBKDFParamRequest initiatorRandom, initiatorSessionId, passcodeId, hasPBKDFParameters, [initiatorSessionParams] PBKDFParamResponse initiatorRandom, responderRandom, responderSessionId, [pbkdfParameters{iterations, salt}], [responderSessionParams] Pake1 pA Pake2 pB, cB Pake3 cA This file is the codec only. The SPAKE2+ arithmetic that produces pA/pB/cA/cB is separate, and on this part it comes from nrf_oberon (nrfxlib/crypto/nrf_oberon/include/ocrypto_spake2p_p256.h), which already ships in every image here.matter_spake2p.hSPAKE2+ glue: PBKDF2, transcript, confirmations. SPAKE2+ is how PASE turns a short setup passcode into a session key without ever putting the passcode on the wire. The elliptic-curve arithmetic is NOT here: it comes from nrf_oberon, which ships four primitives that do exactly the operations SPAKE2+ needs. Everything around them -- deriving w0 and w1 from the passcode, building the transcript, and turning it into the confirmation values -- is this file, and all of it is byte manipulation and hashing that the host suite can check. w0, w1 PBKDF2-HMAC-SHA256(passcode, salt, iterations) -> 80 B -> two 40-byte halves, each reduced mod the P-256 group order TT ten elements, each prefixed with its length as a little-endian uint64: context, "", "", M, N, pA, pB, Z, V, w0 Ka|Ke SHA256(TT), first half and second half KcA|KcB HKDF(Ka, "ConfirmationKeys") cA, cB HMAC(KcA, pB) and HMAC(KcB, pA)matter_btp.hBTP, the Matter commissioning transport over BLE GATT. A Matter message is far larger than a BLE ATT payload, so BTP chops it into fragments, numbers them, and acknowledges them. This file is the framing only: no GATT, no Zephyr, no timers. The 0xFFF6 service that carries it is a separate piece. handshake req 0x65 0x6C versions[4] mtu:u16 window:u8 (9 bytes) handshake resp 0x65 0x6C version:u8 fragment:u16 window:u8 (6 bytes) data fragment flags:u8 [ack:u8 if A] seq:u8 [len:u16 if S] payload Little-endian, like the rest of Matter.matter_mrp.hMessage Reliability Protocol: backoff, retransmit, dedup. Matter runs over UDP, so reliability is the application's problem. MRP is the answer: mark a message as needing an acknowledgement, retransmit on an exponential backoff until it is acked, and drop counters you have already seen. Two objects with two different lifetimes, deliberately not merged: struct matter_mrp_window per SESSION — duplicate suppression struct matter_mrp per EXCHANGE — one un-acked message, one owed ack NO TIMERS LIVE HERE. Every entry point takesnow_msand the object only ever computes deadlines, so the caller owns the timer and this layer stays testable on the host with a fake clock. That is also the stage 0 work-queue constraint honoured by construction: a module that never arms a timer cannot accidentally arm one on k_sys_work_q, which was measured at 3,568 of 4,096 bytes with the reader running.matter_msg.hMatter message header and protocol (exchange) header. Two headers, one wire format. The message header is the part that travels in clear even on a secure session; the protocol header sits at the front of the (decrypted) payload and names the exchange the message belongs to. message header flags:u8 session_id:u16 security_flags:u8 counter:u32 [source_node_id:u64 if S] [dest:u64|u16 by DSIZ] protocol header exchange_flags:u8 opcode:u8 exchange_id:u16 [vendor_id:u16 if V] protocol_id:u16 [ack_counter:u32 if A] All little-endian.matter_status.hreturn codes shared by every woz_matter layer.matter_thread.hthe seam between a commissioner's dataset and a radio. matter_clusters.c is platform-agnostic C11 and the host suite compiles it without Zephyr, so it cannot call OpenThread. It calls these two instead; the port forwards them to otDatasetSetActiveTlvs() and otThreadGetDeviceRole(), and the host suite substitutes a double whose answers a test can choose. The split into start and wait is deliberate. Apple sends AddOrUpdateThreadNetwork, then ArmFailSafe, then ConnectNetwork, and the attach can begin at the first of those rather than the last -- a Thread attach costs seconds and the round trips in between are free.
woz_nfcmodules/woz_nfc10 modules
transport_pn532.cppWozNfc backend driving an NXP PN532 reader. A dedicated thread owns the chip: it runs the discovery loop (RF field on, one Apple ECP broadcast, one 106 kbps type A activation attempt, field off, sleep) and, once an ISO-DEP User Device is activated, performs the blocking APDU round trips. Stack callbacks (CreateSession / HandleSessionData / DestroySession) are posted to the Aliro workqueue so the stack observes the same threading as with the upstream RFAL transport, and Send() stays asynchronous: it hands the APDU to the thread and returns. The ECP frame layout mirrors modules/woz_aliro_ecp (the RFAL-path emitter): 8-byte Aliro ECP v2 header, 8-byte provisioned reader identifier, CRC_A. The PN532 cannot inject raw frames mid-discovery the way RFAL's proprietary poll hook can, so the frame is broadcast with InCommunicateThru while the CIU CRC is switched off, between activation attempts — the same cadence a matching iPhone expects: ECP beacon, then WUPA.pn532_bus_spi.cZephyr SPI glue for the PN532 host protocol. PN532 SPI framing (UM0701-02 §6.2.5): every transaction opens with a one-byte command — 0x01 DATAWRITE (host→PN532 frame), 0x02 STATREAD (read a one-byte status; bit0 set = a response frame is ready), 0x03 DATAREAD (PN532→host frame). The interface is byte-wise LSB-first, which the nRF5340 SPIM does in hardware (SPI_TRANSFER_LSB), so buffers hold ordinary MSB-order bytes here and the peripheral flips them on the wire. Each command, status poll, and frame read is its own CS-cycled transaction (the same shape as the Adafruit/ESPHome PN532 drivers). DATAREAD clocks its command byte and the complete response through one contiguous SPIM transfer. The chip re-presents the current frame on each DATAREAD, so reading more bytes than a frame holds is harmless as long as CS is dropped between frames — with one exception the caller enforces: the ACK read is kept short (PN532_ACK_READ_LEN) because the response follows it immediately and a long over-read would clock it away. Readiness is polled with STATREAD unless irq-gpios is wired (active low = frame ready), in which case a GPIO edge wakes the waiting thread.pn532.cPN532 host-protocol driver. See pn532.h. OS-free: no Zephyr headers, no allocation, no sleeping — waiting is delegated to the bus wait_ready op.pn532_apdu.c@file pn532_apdu.c PN532 APDU command planner: parse ISO 7816-4 APDU structure (Case 1-4, short/extended), emit passthrough or fragmented transport frames, handle GetResponse for extended data retrieval.transport_none.cppWozNfc backend for boards with no NFC frontend: polling never starts and no NFC session is ever created, so Send()/Terminate() are unreachable in a correct run; Send() reports invalid state defensively.transport_rfal.cppWozNfc backend forwarding to the add-on's ST25R/RFAL transport unchanged.transport.hWoz NFC transport seam. One reader backend is selected at build time (Kconfig choice WOZ_NFC_TRANSPORT): the upstream ST25R/RFAL transport, the in-tree PN532 transport, or none. The add-on application calls these five functions instead of a concrete transport class; the selected backend supplies the definitions. The semantics mirror the upstream NfcTransportRfal public API exactly: - Init(): bring up the bus/PAL. Failure is logged by the caller but not fatal. - Start(): begin polling for a User Device. May be called again after Stop(). - Stop(): cease polling and switch the RF field off. - Send(): asynchronous. Queues one APDU for the activated device and returns; the response is delivered later via AliroStack::HandleSessionData() from the Aliro workqueue. Returns ALIRO_INVALID_STATE when no device is activated. - Terminate(): the stack is done with the session; drop the device and return to polling. Does not call back into the stack. The backend owns the session lifecycle in the other direction: on ISO-DEP activation it calls AliroStack::CreateSession(ConnectionHandle::Nfc()), on device loss or exchange failure DestroySession(), both from the Aliro workqueue, matching the upstream RFAL transport's threading.pn532.hNXP PN532 host-protocol driver: frame codec and the command subset needed by the Aliro reader transport. Bus-agnostic and OS-free — all I/O goes through injected bus operations, so the whole layer compiles and runs in the host test suite against a scripted fake bus. Protocol reference: NXP UM0701-02 (PN532 User Manual).pn532_apdu.hPN532-specific ISO 7816 APDU adaptation. The Aliro stack (including the prebuilt library) negotiates sizes with the User Device, but has no API for the reader controller's smaller local limit. This adapter keeps that hardware constraint at the transport boundary.pn532_bus.hBus binding for the PN532 driver. One implementation is compiled in per build (currently SPI: pn532_bus_spi.c). The transport uses only these neutral names, so swapping the physical bus never touches pn532.c or transport_pn532.cpp.
woz_portmodules/woz_port/include2 modules
woz_uwb/aliromodules/woz_uwb/src/aliro18 modules
aliro_uwb_msg.csetup/notification message codec.aliro_device_uwb.cDevice-side UWB ranging-service setup codec: parses the reader's M1 and M3 setup messages, picks the device's answer to M1 (select_m2), and builds the M2 and M4 replies. The inverse of the reader path in aliro_uwb_msg.c, written over the same TLV parser and builder helpers. No crypto and no session state, so a host loopback can drive the real reader codec end to end.aliro_uwb_session.cper-session lifecycle and state machine.aliro_uwb_adapter.creader-context lifecycle.aliro_uwb_msg_builder.cbig-endian TLV message builder.aliro_uwb_msg_parser.cTLV attribute parser and big-endian reads.aliro_uwb_msg.hmessage framing accessors, dispatch and builders.aliro_uwb_msg_builder.hbig-endian TLV message builder.aliro_uwb_msg_parser.hTLV attribute iteration and big-endian reads.aliro_uwb_msg_spec.hUWB ranging-service framing constants.aliro_uwb_adapter.hreader-device public interface.aliro_uwb_session.hper-session public interface.cherry.hCherry core (context + device-capabilities) interface.cherry_ccc.hCCC/Aliro-session interface (seam the adapter drives).aliro_device_uwb.hDevice/initiator side of the UWB ranging-service setup codec: the interface for parsing the reader's M1 and M3 and building the device's M2 and M4. Declares the decoded views of M1 and M3, the parameter structs the two builders take, and select_m2, which chooses a config and slot layout from what M1 offered. Pure TLV, no crypto and no session state, so it is host-testable against the reader's own codec by loopback.aliro_uwb_internal.hprivate context types and shared helpers.cherry_session.hgeneric base-session interface.cherry_common.hdiagnostics config struct and report forward decl.
woz_uwb/cccmodules/woz_uwb/src/ccc17 modules
ccc_shim_rx.cresponder-RX CCC STS substitution: woz_uwb_arm_rx() programs the CCC STS on each RX-arm; target only.cherry_ccc_shim.ccherry_ccc_* seam (Aliro responder) implemented over the lock-native FiRa MAC; maps each call onto woz_uwb_facade.ccc_shim_wrap.cper-frame STS interception: woz_uwb_set_sts_iv() substitutes the CCC STS for the FiRa MAC; target only.ccc_session.cAliro/CCC ranging seam implementation. See ccc_session.h.ccc_sts.cDW3000 STS register load for the CCC ranging path.ccc_mac.cUWB MAC: hopping sequence, SP0 frame codec, ranging schedule.ccc_shim.cCCC STS substitution core (implementation).ccc_crypto_mbedtls.cAES-ECB block via mbedTLS, backing the CCC key schedule on SoCs without a PSA provider (e.g. ESP32-S3).ccc_crypto_psa.cOn-target AES-ECB block (PSA/CC312) backing the CCC key schedule.ccc_kdf.cUWB key schedule + SP0 Pre-POLL frame codec.aliro_round_config.hone knob for the CCC ranging round's responder count.ccc_kdf.hUWB ranging key schedule + SP0 frame crypto (CONFIG_WOZ_ALIRO). Turns the 32-byte URSK into the per-ranging-cycle keys the DW3000 STS engine and the SP0 frames consume, over a single AES block-encrypt primitive.ccc_mac.hCCC UWB MAC layer: ranging-round scheduling, SP0 frame codec, DS-TWR.ccc_shim.hmap a per-frame STS index to the (dURSK, STS-V) pair the DW3000 STS engine loads.aliro_kdf.hUWB Ranging Secret Key (URSK) length.ccc_session.hAliro/CCC ranging seam: map an Aliro session's URSK + M1-M4 setup to ccc_ran_params.ccc_sts.hload a CCC ranging PPDU's STS key + IV into the DW3000 STS engine.
woz_uwb/drivermodules/woz_uwb/src/driver9 modules
uwb_rxdiag.cDiagnostic RX/TX event tallies + ranging heartbeat.uwb_isr.cDW3000 interrupt-callback registration (implementation).uwb_min.cDW3110 bring-up driver (implementation).uwb_cirdiag.cCIA RX-diagnostics latch + [ALAB] emitter (channel-impulse Stage 0/1). Split the work across the two contexts the ALAB contract demands: the RX callback only latches registers into a snapshot (uwb_cirdiag_capture, plain stores + one SPI read), and a task-side uwb_cirdiag_flush formats/prints the line. On the nRF the flush runs on the sysworkq (uwb_rxdiag.c submits it); on the ESP32 the pinned ISR-service task calls it after its IRQ drain loop, so capture and flush are sequential there. A seqlock covers the one real race (nRF: a new capture preempting a flush mid-copy): torn snapshots are dropped, the next reception re-latches. Stage 1 adds an independently-armed windowed-CIR dump: when armed, capture also reads a fixed window of Ipatov complex taps centred on the first-path index into the snapshot. The taps are NOT printed on the RX/flush path — a full window is ~64 serial lines per reception, enough blocking UART to overrun the ranging slot and stall a live walk-up. Instead flush appends each window to a small RAM ring (the last CIRDIAG_RING_RECS receptions), and the taps are drained toev=uwb.cirlines only when the dump is disarmed (uwb_cirdiag_dump_set_enabled (false)) — that runs in console/task context after the walk-up, so the unlock is unaffected while capturing. Deferring the printing was necessary but not sufficient: the window READ is itself too long to sit inside a live ranging block, where the responder still owes a POLL or Final reception. The shims pass that down as deadline_pending and the window is taken only on the Final. Nor was that sufficient: the accumulator cannot be read at all while the receiver is up, and the shim re-arms an SP0 listen the moment the Final is serviced, so the read has to happen BEFORE that (the shims gate it on ccc_shim_rx_awaiting_final). Doing it on every block then cost every range, so uwb_cirdiag_window_due decimates it to one Final in CIRDIAG_CIR_EVERY.uwb_selftest.cKconfig-gated one-shot UWB init self-test (no iPhone).uwb_min.hMinimal DW3110 (DWM3000EVB) hardware bring-up driver.uwb_rxdiag.hRead-side accessors for the RX event tallies + log stream.uwb_seam.hthe DW3000 entry points the CCC engine owns. Four decadriver calls carry engine behaviour that no caller may skip: arming RX must program the CCC STS for the slot, loading an STS-IV must substitute the CCC STS-V, registering callbacks must insert the Pre-POLL shim, and (re)configuring the PHY is traced. Every call site in this module goes through the helpers below instead of <deca_device_api.h>, so a site added later cannot quietly bypass any of it. scripts/check-uwb-seam.sh enforces that mechanically. Under CONFIG_WOZ_ALIRO the engine supplies the definitions: woz_uwb_arm_rx ccc_shim_rx.c program the CCC key/IV, then arm RX woz_uwb_set_sts_iv ccc_shim_wrap.c substitute the CCC STS-V per frame woz_uwb_set_callbacks uwb_rxdiag.c insert the tally + Pre-POLL shims woz_uwb_configure_phy uwb_rxdiag.c log the PHY configuration The ESP32 port omits uwb_rxdiag.c (it is k_work-based) and supplies the last two from port/woz_seam_stubs.c instead. Below the CONFIG_WOZ_ALIRO tier there is no engine to reach, so each helper inlines to the plain decadriver call. The implementations are free to call the decadriver directly — that is how they reach the hardware, and how a site that has already programmed the STS itself (the self-rearm paths in ccc_shim_rx.c) opts out on purpose.uwb_isr.hDW3000 interrupt-callback registration (public surface).
woz_uwb/facademodules/woz_uwb/src/facade12 modules
woz_uwb_facade.cUWB facade: binds the CCC credential-based STS engine to the DW3000 radio, exposes Aliro DS-TWR responder start/stop and range query, and manages platform dependencies (HFCLK boost, SPI init, callbacks).flight_recorder.c@file flight_recorder.c Binary flight-recorder format: framed records (magic, metadata, configuration, events, end) with little-endian integers and truncation handling; read/write operations with overflow detection.woz_alloc.hMemory allocation and timing facade: qmalloc, qcalloc, qfree wrap the platform heap; qrtc_get_us returns monotonic microseconds since boot.woz_util.hNo module docstring. First commit: "port: replace the Zephyr compat shims with a neutral woz_port.h contract".woz_uwb_facade.hPublic header for UWB facade: exposes Aliro DS-TWR responder lifecycle and range query; the CCC engine is bound and unbound via internal ursk and stop calls.flight_recorder.h@file flight_recorder.h Capture and replay UWB frames and session configuration from a walk-up to a host for analysis and replay. Records endpoint identity, status registers, frame data, and timing metadata into a fixed-size ring buffer; provides reader and writer interfaces for host tools.woz_bytes.h@file woz_bytes.h Byte-order utilities: read/write 16-bit and 32-bit integers in little-endian or big-endian order.woz_diag.hDIAGK(): gate for verbose UWB bring-up diagnostics.uwb_cirdiag.hPer-reception CIA first-path/STS diagnostics stream (channel-impulse Stage 0). The RX callback latches the DW3000's CIA diagnostic bank (Ipatov/STS first-path index, F1..F3, power, peak, STS quality, xtal offset); task context emits it as one "[ALAB] t=<us> ev=uwb.diag ..." line for tools/aliro_lab.py. OFF at boot; armed at runtime (nRFaliro cir on, ESP32 rides thelab ongate).trace.hStructured [WOZ_TRACE] emit helpers, gated on CONFIG_WOZ_E2E_TRACE.woz_logfmt.cPRETTY-gated high-res timestamp + compact colored log line.woz_logquiet.cPRETTY-gated runtime muting of benign upstream error spam. The stock Matter/BLE stack logs several non-fatal conditions at LOG_ERR/LOG_WRN (red/yellow): mDNS advertiser "incorrect state" churn, "Long dispatch time" perf notes, unsupported-attribute reads, the "No valid legacy adv to stop" BLE double-stop, and the empty-slot "Failed to get Access Document at index: 0" the access layer emits on first contact. All are expected on this bare DK bring-up and every one is proven benign by the healthy unlock that follows. A compile-time level cut can't remove just these: each noisy source shares its CONFIG_*_LOG_LEVEL with a source whose INFO lines drive the demo narrative (access_document shares CONFIG_DOOR_LOCK_APP_LOG_LEVEL with access_manager's "ACCESS GRANTED"/ranging lines; bt_adv shares CONFIG_BT_HCI_CORE_LOG_LEVEL), and a threshold below ERR still lets ERR through. So mute per-source at runtime. Reversible: compiled only under CONFIG_WOZ_PRETTY_SHELL (PRETTY=1). Drop PRETTY and every one of these lines returns for raw diagnosis. Needs CONFIG_LOG_RUNTIME_FILTERING=y (set in ports/nrf5340dk/overlays/woz-pretty.conf).