# Security status before the open-source release

*12 October 2026 · AIW 1.5.0 · written against the source in this repository*

The audit documents in this folder (passes 1 to 9 and the retest) are records of
what was tested at the time. They are left as they were written. This page says
what is true **now**: what was checked again, what was found, what each feature
has and has not been tested for, and what is still open.

**Who did this review.** Like the earlier passes, it was carried out by an AI
agent, and by the same agent that made the changes described below. It is a
re-check with evidence, not an independent audit. AIW has not been audited by an
outside security firm.

**"Tested" does not mean the same thing for every feature.** The tests below
attack AIW's own gates with mock nodes: they show that a request, or the AI
guide, cannot make AIW send something the user did not confirm. They do not
show that a real payment on a real network arrives correctly. The
[readiness table](#what-each-feature-has-been-tested-for) says which features
have had which kind of test.

## What was run

Everything here was run on 12 October 2026 against AIW started from source in a
sandbox: a throwaway data folder, no real wallets, mock nodes that record what
reaches them, Docker unreachable. No test touched a real wallet or network.

| What | How to repeat it | Result |
|---|---|---|
| Unit tests | `npm run test:unit` | 150 of 150 pass |
| of which: the AI guide's spending boundary, attacked with a scripted hostile model | `tests/unit/chat-spend.test.mjs` | 26 pass |
| of which: one request alone must never move money, every realm | `tests/unit/money-routes.test.mjs` | 33 pass |
| of which: what was confirmed is what runs, and it runs once | `tests/unit/confirm-binding.test.mjs` | 19 pass |
| of which: every way a stored secret could get out | `tests/unit/secret-egress.test.mjs` | 9 pass |
| Browser tests | `npm run test:e2e` | 25 pass, 8 not written yet |
| Release gate | `npm run release:gate` | **Fails**, as it should: see below |
| Pass 9 regression, against a mock Monero node | `tests/security/audit9-regression.mjs` | 8 of 8 pass |
| Litecoin and Ethereum realm regression | `tests/security/realms-ltc-eth.mjs` | 14 of 14 pass |
| Known advisories in dependencies | `npm audit` | 0 in what ships; 8 moderate, all in the build tool |

The unit and browser tests also run on every push and pull request.

## Why the method changed

The nine passes read the code route by route. None of them noticed that stored
wallet passwords were being returned by the wallet list. More passes of the same
kind would not have fixed that, so three tests were added that do not depend on
anyone reading the right line:

- **Secrets, by canary.** A marked value is planted in every place AIW stores a
  secret (each password, PIN and API key in `.env`, the node logins, wallet
  passwords, a Monero seed, an Ethereum seed, private key and keystore
  password). The test then collects everything AIW emits by every way out and
  searches it for the marks: all 169 API paths signed in and signed out, the
  live status stream, error replies, files under the web root, everything sent
  to the AI model in each realm, the server log, and every request to another
  computer. A secret may travel to exactly one place, the service it belongs
  to. Re-introducing the original bug on purpose makes the test fail, as does
  logging a password.
- **The AI guide, by attack.** The model is replaced by a script that emits
  whatever a hijacked or mistaken model would: broadcasts, key exports,
  requests to this computer, several payments at once. A mock wallet records
  what reaches it. Each attack runs through both chat pipelines.
- **Every money route, by one blind request.** Each route that can move money,
  in every realm, is sent a single signed-in request with every "yes, I am
  sure" field filled in and nothing prepared beforehand. Recorders standing in
  for each node and service show whether anything was sent.
- **Confirmation, by a reviewer's checklist.** An outside reader of this page
  listed the outcomes to demand: prepare A then B and confirm A; change
  anything after the preview; double-click or replay; cancel then reuse;
  confirm from an older app build. Each is now a test, with "double-click"
  meaning several requests in flight at the same moment.

## What these tests found

All of the following were found on 12 October 2026 and are fixed in 1.5.0.

| # | Problem | Fix |
|---|---|---|
| 1 | **Confirm could send a different payment from the one on the card.** If the AI guide prepared two transfers in one reply, confirming the first card (0.01 XMR to one address) sent the second (5 XMR to another). No attack was needed; "pay Alice and Bob" was enough. Shown with Monero; Bitcoin sends and staged orders were built the same way. | Every prepared action has a random id that the Confirm click must return. A reply that asks for more than one action needing confirmation is refused whole. |
| 2 | **The AI guide could have a Bitcoin or Litecoin node sign a transaction.** A signed transaction can be broadcast by anyone who sees it, without any Confirm click. | Signing and raw-broadcast methods are refused for chat and for the raw node proxy. |
| 3 | **A prepared Monero transfer, already signed, was returned to the page and to the AI model.** | The signed data stays on the server; the reply carries amount, fee and hash only. |
| 4 | **Sweeping a whole wallet took one request** in Monero, Bitcoin and Litecoin, where an ordinary send already required a matching preview. | A sweep is accepted only for the address the Send screen just showed, once. |
| 5 | **A Lightning payment took one request,** and the invoice paid did not have to be the one previewed. | A payment must match the invoice this session previewed, once. |
| 6 | **The AI guide's web lookups accepted plain `http` and followed redirects,** so someone on the network path could feed it forged data. | `https` only, no redirects. |
| 7 | **The atomic-swap tools were downloaded as "whatever is newest" and run without any check.** | One release is pinned by SHA-256, after checking its signatures by hand; anything else is refused. (The newest upstream release also ships no Windows build, so the old behavior would have failed on a new install.) |
| 8 | **Overlapping Confirm requests each went through.** Five at the same moment sent a Bitcoin payment prepared in chat five times. | A prepared action is taken out of the store before it is sent; one request wins and the rest are refused. |
| 9 | **A payment prepared in one wallet could be confirmed after switching to another.** | Changing wallet or network voids everything waiting for a Confirm, in every realm. |
| 10 | **A payment the wallet accepted, whose answer was then lost, was reported as "failed".** Nothing stopped the user from sending it again. | AIW tells apart "the node said no", "the node was never reached" and "the node was handed the payment and went silent". The last is reported as outcome unknown, with the transaction to look for where there is one, and the card cannot be used again. |
| 11 | **Switching coin realm left a prepared payment confirmable** after switching back. | Realm switches void pending actions, like wallet and network switches. |
| 12 | **Stored wallet passwords were sent to the page** (`/api/wallets`). | Stripped on the server. |
| 13 | **The wrong-password lockout did not apply through Tailscale.** | Devices reaching AIW through the proxy are counted and locked out separately. |
| 14 | **Passwords, PINs and API keys were stored in plain text.** | Sealed with a key the operating system keeps for the signed-in user. |
| 15 | **Unfinished features were reachable.** | Off unless switched on per install. |
| 16 | **The dashboard loaded its typefaces from Google.** | Bundled; a test fails if any page contacts another host. |

The secrets test found nothing further once item 12 was fixed.

## How far "one request wins" goes

Fix 8 rests on one fact: looking up a prepared action and removing it happen in
the same uninterrupted step, so a second request cannot find it. That is true
because the AIW server is a single process with a single thread of JavaScript;
there are no workers and no second server process. Where a route has to wait for
something between looking the action up and using it, it checks afterwards that
the action is still the one it found.

This was tested with five simultaneous requests for a Monero transfer from chat,
a Bitcoin send from chat, a Strike withdrawal and a Monero send from the Send
screen. The Bitcoin, Litecoin and Ethereum Send-screen routes use the same
pattern and were checked by reading, not by a test, because the mock nodes for
those cannot build a preview. The guarantee does not extend to any future design
with more than one process handling confirmations; that would need a real lock.

## What each feature has been tested for

- **Gate tests** are the sandbox tests above: can a request or the AI guide
  make AIW send without the user confirming what they were shown?
- **End to end** means a real transaction on a test network with this version.

| Feature | In the first release | Gate tests | End to end, this version |
|---|---|---|---|
| Monero: send, receive | On | Yes | Not repeated. The on-chain proofs in the retest are from an earlier build |
| Monero: sweep | On | Yes | No |
| Bitcoin: send, sweep (own node) | On | Yes | No |
| Litecoin: send, sweep | On | Yes | No |
| Ethereum: send | On | Yes | No |
| Lightning: pay | **Off** | Yes | No |
| Lightning: move balance on-chain | **Off** | Partly: it needs a typed confirmation and can only pay the user's own Bitcoin wallet | No |
| The AI guide (chat) in all four realms | On | Yes | Not applicable |
| Atomic swaps (Bitcoin ⇄ Monero) | **Off** | Staging and the tool download: yes. The swap itself: no | No |
| Orders with swap and exchange services | **Off** | Staging: yes. The services themselves cannot be tested offline | No |
| Strike: buy, withdraw | **Off** | PIN and staging: yes | No |
| Light setups (public servers instead of own nodes) | On, as a setup choice | Same routes as above | No |
| Phone access through Tailscale | On | Lockout: yes | By the author, by hand |
| Seed reveal, wallet creation | On | Secrets test: yes | No |
| Goblin Card, Queen's Decree, hardware wallets, scheduled purchases, mining | **Off** | Blind-request test only | No |

### The rule, and the gate that enforces it

A feature that moves money is either switched off by default, or it has
recorded evidence of an end-to-end run on a real test network against the exact
installer being released. A passing mock suite does not exempt a feature.
`npm run release:gate` checks this from
[`e2e-evidence.json`](e2e-evidence.json) and refuses when anything is missing.
It blocks the publication path rather than advising: a version tag cannot be
pushed while it fails, and the same check runs again on GitHub. Evidence counts
only for one frozen installer, identified by its SHA-256, run with the shipping
defaults; a rebuild is a new candidate. Failed and unresolved runs are recorded
as well, and the latest record decides. Transfers prepared by the AI guide are
counted per realm. The gate's own rules have tests
(`tests/unit/release-gate.test.mjs`).

Lightning, atomic swaps, exchange orders and Strike were switched off on
12 October 2026 under this rule. Switched off means their routes answer "not
found", the AI guide's tools for them are refused and nothing of theirs runs
in the background; a test checks all three.

**The gate currently fails.** Sending, receiving and sweeping in the four
realms, and transfers prepared by the AI guide, are switched on and have no
evidence record yet. Until they do, this build is a development checkpoint and
not a release.

So: every enabled money path has been attacked at its gate, and none has had an
end-to-end run on a test network with 1.5.0. The gate tests say AIW will not
send what you did not confirm. They do not say that Lightning, swaps, exchange
orders or Strike complete correctly; those rest on the author's own use during
development. The checks to run by hand on a real test network before release are
in [`tests/RELEASE-CHECKLIST.md`](../../tests/RELEASE-CHECKLIST.md).

## Claims from the earlier passes, checked again

| Claim | Status now | Evidence |
|---|---|---|
| Nothing behind the sign-in gate is served without a session | Holds | Every POST route and all 169 API paths were requested signed out: none acted, none returned a secret |
| One session cannot confirm, send or cancel what another prepared | Holds | Pass 9 regression T-002; `chat-spend` (a second session, even with the id) |
| A malformed or wrong-network destination is refused first | Holds | Pass 9 regression T-009 |
| A send needs a matching preview first, in every realm | Holds, and now for sweeps and Lightning too | `money-routes` |
| Key-export and spending methods are blocked on the raw node proxies | Holds, and now signing as well | `money-routes`, R-2, R-3 |
| The Ethereum keystore cannot be silently overwritten | Holds | R-4 |
| Secrets never reach the page, the model or the log | Holds after fix 12 | `secret-egress` |
| The AI guide cannot move money by itself | Holds after fixes 1 to 3, 6, 8 and 9 | `chat-spend`: broadcast, sign, key export, local requests, several actions at once, stale and foreign confirmations |
| AIW listens on the local computer only by default | Holds | Read from source; not re-run dynamically |

## Statements in the older documents that are no longer accurate

- *"`X-Forwarded-For` is never trusted."* It is still never used to treat a
  request as local. It is now read, only for a request that arrived through a
  proxy on the same computer, and only to tell remote devices apart for the
  lockout.
- *AIW described as a Monero and Bitcoin wallet.* Litecoin and Ethereum came
  later and were not covered by passes 1 to 9.
- *`core/agent-sdk-chat.mjs`* has been removed; chat goes through
  `ui/server.mjs` only.
- *Node images.* The Bitcoin, Litecoin and Monero node images are built from
  each project's official release, with the download's SHA-256 pinned.
- *"All findings fixed" and "9/9 pass".* True of what those passes looked for.
  The table above lists what they did not find.
- *Reporting a vulnerability.* The form on the website is not connected.
  [SECURITY.md](../../SECURITY.md) is the current route.

## What is still open

| | Why it matters |
|---|---|
| **No independent audit.** | Every pass, including this one, was run by AI agents directed by the author. The problems in the table above were all found in two days by changing the method, which suggests there are more |
| **No end-to-end run on a test network for this version.** | See the readiness table |
| **The browser can still be the weak point.** | The gates assume the dashboard shows the user the truth. A script running inside the dashboard page could prepare and confirm in two requests. No such injection is known; the page loads nothing from other sites, and chat does not render links or images |
| **The Windows installer is not code-signed.** | Its origin rests on the signed checksum list (see [verifying.md](verifying.md)) |
| **Pinned downloads are checked by hash, not by signature, at install time.** | The swap tools and the node releases were each checked against the project's signing key by hand when the pin was set. A wrong pin would not be caught by the build |
| **The Tor image is a third-party image, unpinned.** | It holds no keys and is not in the path of funds |
| **Run from source without the desktop app, nothing is sealed.** | `npm start` leaves `.env` in plain text |
| **An Android build contains the dashboard password.** | The app file must never be shared; it is never published |
| **The held-back features are unreviewed in their current form.** | They are off by default for that reason |
| **Software running as the signed-in user** can ask the operating system to open sealed values and can read a running AIW's memory. | Sealing protects the files, not a compromised account |

## Accepted risks, as they stand

- **Sign-ins on the computer itself are never locked out.** Every other device,
  including one reaching AIW through Tailscale, is locked out after five wrong
  passwords.
- **The app is not packed into a single archive** (`asar: false`), so there is
  no archive integrity check.
- **AIW never updates itself.** It tells the user when a newer release exists.
- **Moving a Lightning balance on-chain needs a typed confirmation, not a
  preview.** Its destination is always the user's own Bitcoin wallet.
- **An unknown outcome stays unknown until the user looks.** AIW does not try to
  work out by itself whether a payment whose answer was lost went through, and
  it never re-sends. Chat cards and Send screens both show it under its own
  heading, "Payment outcome unknown", never as a failed transaction. Finding
  nothing after a block is evidence, not proof that it will never appear.
