Metadata-Version: 2.4
Name: aleth-reader-host
Version: 0.1.0
Summary: Native-messaging host for the Aleth reader browser extension: the browser launches it, it writes what you read to the memory engine on your own machine over the public wire. Stdlib plus aleth-client, zero engine import.
Author: Aleth Authors
License: Apache-2.0
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aleth-client<2,>=1.1
Dynamic: license-file

# aleth-reader

A browser extension that remembers what you read, into the memory engine
running on your own machine. No account, no server, no upload.

It is made of two pieces:

- **`extension/`**: the part in the browser. It watches what is on screen,
  scores which blocks are reading material, and sends the new sentences out.
- **`host/`**: a small Python program the browser starts for it. This is the
  only way the extension can reach anything outside the browser, and all it
  can reach is the memory engine on this machine.

The extension has no network permission at all. It cannot fetch a page, call
an API, check for updates or send a report, because it holds no permission
that would let it and there is no such code in it. That is a stronger claim
than "it only talks to localhost", and there is a command below that measures
it rather than asking you to believe it.

## What it stores

Only what you actually looked at. It polls the visible part of the page,
scores each block the way a reader-view algorithm does (how much text it
holds, how much of that text is links, whether its own class name says
`article` or says `cookie-banner`), and keeps what passes. Read half a page
and half a page is what is remembered.

It stores the text verbatim with the page's URL and title attached. Nothing is
summarised, and no language model sees it.

It de-duplicates by sentence, so opening the same article tomorrow stores
nothing, and an article that grew a paragraph stores only that paragraph.

**There are no per-site rules.** Nothing in the reading path knows the name of
any website. The same code runs on a news site, a wiki, a PDF and a page
nobody has ever seen before.

## What it never stores

- **Anything in a sensitive category.** Finance, health, adult, sign-in pages
  and webmail are blocked before anything is read. The list is evaluated
  inside the extension, on your machine. No server is ever asked about a
  domain you visited, which is the point.
- **Any site you put on the never-remember list.** One click on the chip.
- **Anything at all while it is paused.**
- **Chat threads**, unless you switch bridge mode on yourself (see below).
  Reading mode stands down on a chat page.

The sensitive-category rules are a **port** of the ones in
`clients/aleth-ingest/aleth_ingest/browser_history.py`, which is the source of
truth. Both are driven by the same table of test cases from two test suites,
so if one drifts from the other, a test goes red.

The categories are a default, not a verdict. The list is short on purpose
(short enough to read), and it will not know about your bank if your bank's
name carries no money word in it. Use the never-remember list for those.

## The chip

A small chip sits in the corner of the page and says which of three things is
true:

| chip | meaning |
|---|---|
| Remembering | this page is being captured |
| Paused | you paused it; nothing is being captured anywhere |
| Blocked | this page will never be captured: a sensitive category, or your own never-remember list |

Click the chip to pause or resume. Hover it for a **Never here** button. The
toolbar icon carries a badge with the same state, so you can also see it
without the page.

There is a fourth state the chip shows only in bridge mode: **Capture broken**.
It means a chat page changed its shape and the bridge can no longer find the
parts it needs. It is loud on purpose; see below.

## Bridge mode (chat pages)

Off by default, and it stays off until you turn it on. When it is on, on a
chat page (ChatGPT, Claude, Gemini, or any page with a compose box and a
conversation in it) the extension:

- stores each turn **as it happens**: your prompt when you send it, the
  reply when it stops growing;
- **offers** relevant memory into the compose box before you send. It shows
  you what it found and you click Insert or dismiss it. It never edits what
  you are about to send.

Nothing leaves your machine except the prompt you send to the model you chose,
by your own hand, through that site's own code.

**Why "as it happens" matters.** These pages destroy old messages from the
page as you scroll away from them. Reading the thread afterwards reads
whatever survived, and it fails quietly. Capturing the turn while it exists
does not have that problem.

**When a chat page changes its shape.** It will; they all do. There is one
generic adapter (find the text field, find the last reply) and a small table
of per-site refinements, and a check that runs against the real page:

- it runs automatically when bridge mode starts on a page;
- you can run it any time from the popup with **Check this chat page**;
- if it fails, the chip goes to **Capture broken**, the browser console gets a
  line naming which part broke, and the popup's counter goes up.

A failure is never silent. That is the whole design of that part.

## PDFs

Honest answer, per browser:

- **Firefox** renders PDFs with its own viewer (pdf.js), which puts a text
  layer in the page. The extension reads it like any other page, in place,
  and nothing is uploaded.
- **Chrome** renders PDFs with a built-in plugin that a content script cannot
  read. On Chrome, a PDF is **not** captured. Nothing is uploaded either: it
  simply does nothing, and the chip shows the state honestly.

For PDFs on disk on either browser, use `aleth-ingest` instead: it reads local
files directly and does not need a browser at all.

## Install

You need a running Aleth memory engine on this machine (the host finds it
through its discovery file, the same way every other client does).

### 1. The native messaging host

```powershell
cd <path-to>\aleth-reader
pip install -e .
python -m aleth_reader_host install --browser chrome,chromium,firefox
```

Run it from a directory inside the engine's tree, or pass `--discovery-dir`.
The browser starts the host with the *browser's* working directory, so the
installer records where the engine's discovery file is; without that, every
capture fails with "engine is not running" while the engine is running fine.
The line it prints tells you which directory it found, or that it found none.

That writes a small manifest telling each browser what to run and which
extension is allowed to talk to it. On Windows it also writes a launcher
`.bat` (browsers there cannot run a `.py` directly) and a registry value under
`HKEY_CURRENT_USER` pointing at the manifest. Nothing is written for other
users of the machine and nothing needs administrator rights.

See what it would do first with `--dry-run`, and undo it with `--uninstall`.

### 2. The extension

**Chrome** (or Brave, Edge, any Chromium):

1. `chrome://extensions`
2. turn on Developer mode
3. Load unpacked -> pick the `extension` folder

The extension's id is fixed (`kmbfklemccbmfmhldhbjgdcidbiimhpe`), because the
manifest carries a public key. That is what lets the installer name the
allowed extension before you have loaded it. Loading a differently-keyed
build? Pass `--extension-id` to the installer.

Note that `--load-extension` on the command line is refused by current Chrome
whatever flags you pass; loading it through the page above works normally.

**Firefox**:

```powershell
python tools\build_extension.py --browser firefox
```

1. `about:debugging#/runtime/this-firefox`
2. Load Temporary Add-on
3. pick `dist\firefox\manifest.json`

Firefox unloads a temporary add-on when it closes; that is Firefox's rule for
unsigned add-ons, not ours.

### 3. Check it

Open the extension's popup and press **Check the engine**. It should report
the engine's node count. If it does not, the host's log is at
`~/.aleth-reader/host.log`.

## Check the privacy claim yourself

```powershell
python tools\privacy_trace.py --static-only
```

That scans every file that ships for an outbound API (`fetch`,
`XMLHttpRequest`, `WebSocket`, `sendBeacon`, `EventSource`, a remote script
tag). The answer should be none.

To measure a running browser instead, get its process id and:

```powershell
python tools\privacy_trace.py --browser-pid <pid> --host-match aleth_reader_host --duration 30
```

It lists every TCP endpoint the browser's process tree and the native host
hold, split into loopback and remote. With only local pages open, the honest
result is zero remote connections and one loopback connection: the host, to
the memory engine.

A snapshot cannot see a connection that opened and closed between two samples,
which is why it samples repeatedly and why the source scan exists beside it.

## Tests

JavaScript (no dependencies, node's own test runner):

```powershell
cd clients\aleth-reader
node --test "tests/*.test.mjs"
```

Python:

```powershell
python scripts\safe_pytest.py clients\aleth-reader\host\tests\test_framing.py clients\aleth-reader\host\tests\test_host_dispatch.py clients\aleth-reader\host\tests\test_install.py clients\aleth-reader\host\tests\test_ports_parity.py
```

The parity test runs the extension's own JavaScript and compares its verdicts
with the Python original, case by case, for both the blocklist and the URL
normaliser. It fails rather than skips if node is missing, because a parity
test that quietly does not run reports green.

### The fixtures

`fixtures/` holds four local pages: an article wrapped in the usual furniture,
and three pages that reproduce the DOM shape of the three chat products. They
are served over HTTPS by `tools/fixture_server.py` and loaded under mapped
hostnames, so the reading path and each per-site override can be exercised in
a real browser without an account and without touching the real sites. See
`docs/live-proof.md` for the exact commands.

## What has actually been proven, and what has not

Proven in a real browser, with the engine running (see `docs/live-proof.md`
for the commands and the output):

- text captured from a page and then **recalled by meaning from the engine,
  with its source URL**, by a separate process over the public wire;
- a chat turn (the prompt and the reply, separately) captured as it
  happened and recalled the same way;
- the bridge's conformance check passing against all three per-host
  overrides;
- a sensitive category refusing to capture;
- **zero remote connections** from the whole browser tree and the native
  host, under a sealed run.

Not proven, and honestly out of reach here:

- **A day of real reading.** That needs the person whose reading it is.
- **The real ChatGPT and Claude.ai pages.** Those need signed-in accounts.
  The fixtures reproduce each product's DOM shape, which proves the adapter
  and the capture path; they cannot prove the real site still uses those
  selectors today. That is what the conformance check is for, and it can be
  pointed at the real page in one command.
- **Firefox.** There is no Firefox on the machine this was built on. The
  Firefox manifest is written, the build step produces a loadable directory
  and the installer writes the Mozilla registration, but nobody has yet
  watched it load.
- **The PDF path**, for the same reason: the Firefox claim above is
  reasoning about pdf.js, not a measurement.

## What this is not

It is not a scraper. It does not read pages you did not open, it does not
follow links, and it does not fetch anything.

It is not a summariser. It stores what was on the page.

It is not a sync service. What it writes stays on this machine.
