Linux first run, and a website rule that reaches the screenshot #1

Merged
LNK merged 6 commits from claude/linux-first-run into main 2026-09-04 16:19:21 +00:00
Contributor

Six commits on claude/linux-first-run, rebased on main at f6243e6 (the wiki). Every one is green on Linux under scripts/verify.sh in a container: 275 Rust + 86 TypeScript = 361 tests, every stage, exit 0. Windows-only packages and the end-to-end suite are not checked here; verify.ps1 on a Windows machine covers those.

The one to read first — e8dadf2

"Never capture my bank" suppressed the host name and stored the screenshot. Only the extension's report carries the origin; L0 judges the record in hand. The frames collector sends origin: None on both platforms, so the screenshot of a denied site matched the browser's allow rule and went through: painted, blobbed, uploaded. Focus records kept the title the same way. The test that looks like it covers this (a_denied_app_produces_no_pixels_at_all) suppresses because chrome.exe is not allow-listed at all — nothing in the suite had an allowed browser and a denied site, the only configuration the extension exists for.

The gate now remembers the site each browser last reported and stamps it on every later record from that browser that arrives without one, before L0. Joined by browser family, not exe string: the extension can only say chrome.exe, a Mac says Google Chrome, and a string join would pass on Windows and capture the bank on every Mac. The table is closed to the four names the extension can report, so a site is never stamped on an application.

Not closed, and documented: a frame captured between a navigation and the extension's report carries the previous site. Three pipeline tests, all fail against the old code (checked by making the join a no-op).

The Linux run — five commits

Nothing had ever run the second verifier on Linux. It found:

  • ps -o comm= is cut to 15 characters by the kernel, so every collector read as not running and the Stop button found nothing to stop while capture carried on. Linux now reads /proc/<pid>/exe. (e190b41)
  • The Linux arm of data_dir had never been linted; a collapsible if failed -D warnings, so a green Linux run was impossible. (2bfd724)
  • A test asserted a path was outside the system temp dir using a /tmp fixture: true on a Mac, false on Linux. (41be856)
  • The three shared indicator modules are dead code on a platform with no body; one cfg_attr to delete the day there is one. (af9f284)
  • On macOS every batch header's sent_at was 1970-01-01T00:00:00Z, and eight call sites wrote the epoch into uploaded_at. The non-Windows arm of two functions returned the literal. Passes the schema, the lint and the server. Fixed with a test that asserts the value. (2cf7be5)

Seen and deliberately left alone

  • here-win goes red on clippy under Rust 1.98 (chunks_exact_to_as_chunks) and rust-toolchain.toml floats on stable — the Windows gate breaks on the next rustup update with nobody changing anything.
  • No Node floor is declared anywhere; on Node 18 nine pairing tests die on a bare crypto global (pairing.ts:197).
  • here-console and here-indicator cannot be cross-checked for Windows from Linux (SQLite needs the SDK), so those two commits are compiled on Linux only.
Six commits on `claude/linux-first-run`, rebased on `main` at `f6243e6` (the wiki). Every one is green on Linux under `scripts/verify.sh` in a container: 275 Rust + 86 TypeScript = 361 tests, every stage, exit 0. Windows-only packages and the end-to-end suite are not checked here; `verify.ps1` on a Windows machine covers those. ## The one to read first — `e8dadf2` **"Never capture my bank" suppressed the host name and stored the screenshot.** Only the extension's report carries the origin; L0 judges the record in hand. The frames collector sends `origin: None` on both platforms, so the screenshot of a denied site matched the browser's allow rule and went through: painted, blobbed, uploaded. Focus records kept the title the same way. The test that looks like it covers this (`a_denied_app_produces_no_pixels_at_all`) suppresses because `chrome.exe` is not allow-listed at all — nothing in the suite had an allowed browser and a denied site, the only configuration the extension exists for. The gate now remembers the site each browser last reported and stamps it on every later record from that browser that arrives without one, before L0. Joined by browser family, not exe string: the extension can only say `chrome.exe`, a Mac says `Google Chrome`, and a string join would pass on Windows and capture the bank on every Mac. The table is closed to the four names the extension can report, so a site is never stamped on an application. Not closed, and documented: a frame captured between a navigation and the extension's report carries the previous site. Three pipeline tests, all fail against the old code (checked by making the join a no-op). ## The Linux run — five commits Nothing had ever run the second verifier on Linux. It found: - `ps -o comm=` is cut to 15 characters by the kernel, so every collector read as not running and the Stop button found nothing to stop while capture carried on. Linux now reads `/proc/<pid>/exe`. (`e190b41`) - The Linux arm of `data_dir` had never been linted; a collapsible `if` failed `-D warnings`, so a green Linux run was impossible. (`2bfd724`) - A test asserted a path was outside the system temp dir using a `/tmp` fixture: true on a Mac, false on Linux. (`41be856`) - The three shared indicator modules are dead code on a platform with no body; one `cfg_attr` to delete the day there is one. (`af9f284`) - **On macOS every batch header's `sent_at` was `1970-01-01T00:00:00Z`**, and eight call sites wrote the epoch into `uploaded_at`. The non-Windows arm of two functions returned the literal. Passes the schema, the lint and the server. Fixed with a test that asserts the value. (`2cf7be5`) ## Seen and deliberately left alone - `here-win` goes red on clippy under Rust 1.98 (`chunks_exact_to_as_chunks`) and `rust-toolchain.toml` floats on `stable` — the Windows gate breaks on the next `rustup update` with nobody changing anything. - No Node floor is declared anywhere; on Node 18 nine pairing tests die on a bare `crypto` global (`pairing.ts:197`). - `here-console` and `here-indicator` cannot be cross-checked for Windows from Linux (SQLite needs the SDK), so those two commits are compiled on Linux only.
`ps -o comm=` is a macOS answer, not a Unix one. The kernel keeps `comm` in sixteen bytes, so
Linux cuts every name at fifteen characters: `here-collector-input-metrics` reads back as
`here-collector-`, and so does every other collector — a miss and an ambiguity at once. Six of
the console's ten roles are longer than fifteen characters, which is every collector it starts.
So `is_running` reports capture as not running while all of it runs, directly under the Start
button, and `kill_by_name` finds nothing to stop and answers "Nothing was running." while
capture carries on.

That is the same defect `tasklist`'s twenty-five-character column had, in a second disguise.
The test that catches it was already written — it asks whether this test binary is running, and
the harness names it `here_console-<hash>` — it had simply never run on Linux.

Linux now reads `/proc/<pid>/exe`, which is the executable itself and is not truncated, and
handles the `(deleted)` suffix a replaced binary carries, because an upgrade is exactly when
somebody presses Stop. macOS keeps `ps`; `parse_ps` stays compiled everywhere so its tests keep
running, in the form `channels` and `mac_banner` settled on. The shared test now states its
premise, so a harness that shortened the binary's name would say so rather than quietly stop
covering the truncation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`theme`, `cards` and `say` hold what the pill says and how long it says it for, shared so that
two platforms cannot describe the same state differently. A platform with no body of its own
compiles all three and calls none of them, so on Linux every item in them is dead code and the
lint stage refuses the build: five errors, `GROUND_EDGE`, `verdict_style`, `RECENT_LIMIT` and
two structs' fields.

Cfg'ing the modules out would take their tests off Linux with them, which is the wrong trade —
they are portable and they are the reason the modules exist. So the crate carries one
`cfg_attr` that says the platform has no body yet, in the form `channels` and `mac_banner`
already settled on. It is an annotation to delete the day there is a Linux body, not one to
keep and work around.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`base_dir` has three bodies and the lint stage has only ever run over two of them. The third —
`XDG_DATA_HOME`, then `$HOME/.local/share` — is compiled on Linux alone, so it reached
`-D warnings` carrying a collapsible `if` that fails the stage outright. No Linux machine could
run `verify.sh` to the end, on the platform the script's own header says it supports.

Collapsed, and given the two tests the branch never had: that the variable is the answer where
it is set, and that an empty one falls through rather than putting the store at the filesystem
root. `base_dir` rather than `data_dir`, because `Env` holds a mutex and a second one in the
same test would wait on the first for ever.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`the_runtime_directory_is_beside_the_store_and_not_inside_the_system_temp` pointed
`HERE_DATA_DIR` at `/tmp/here-test-store` and then asserted the answer was not inside
`std::env::temp_dir()`. On a Mac that is `$TMPDIR` under `/var/folders`, so the fixture stood
outside it. On Linux `temp_dir()` is `/tmp`, so the fixture stood inside it and the test failed
on its own setup the first time it ran there.

The claim is about the default, so the default is what carries it now: the sockets sit beside
the store, and the store is not in the system temporary directory — on every platform.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`batch::now` and `lib::timestamp` both forked on `cfg(windows)`: the Windows arm called
`here_win::now_rfc3339`, and the other arm returned the literal string
"1970-01-01T00:00:00Z". So on macOS — the platform with a shipped, released build — the batch
header's `sent_at` said the batch left the device on 1 January 1970, and the eight call sites
of `timestamp` wrote that same epoch into `events.uploaded_at`, into `blobs.uploaded_at`, onto
every failed attempt and onto the access-log fetch.

Nothing catches it. `sent_at` is a string in the schema, so a batch stamped 1970 is a valid
batch: it passes `contracts-lint`, it passes the generated TypeScript, and the server accepts
it. `sent_at` is also the only statement the server has about when the device sent anything.

The fork was left over from the move that put `now_rfc3339` in `here-os` — `here-uploader`
has depended on `here-os` unconditionally ever since, so both arms could always have called
the portable one. Same shape, same file, same platform as the fourth copy of `data_dir` that
left the uploader silently dead on a Mac: a non-Windows arm nobody ran.

The test asserts the value rather than the shape, because the shape was never wrong. It fails
against the old code — checked, by putting the stub back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README gives the reason the browser extension exists: without it, "never capture my
bank" cannot be enforced. With it, the rule was enforced on exactly one record. The extension's
report carries the host name in its app context, L0 judges the record in hand, and the report
is suppressed. The screenshot of the same window arrives from the frames collector with
`origin: None` — it has no way to know — and matches the browser's allow rule instead, so the
bank is captured, painted, blobbed and uploaded. The focus record keeps the window title the
same way. Two senses describing one window, and no rule could read across them.

The test that looks like it covers this, `a_denied_app_produces_no_pixels_at_all`, suppresses
for a different reason: `chrome.exe` is not on the allow-list at all. Nothing in the suite had
a browser that was allowed and a site that was denied, which is the only configuration the
extension is for.

The gate now remembers the site each browser last reported and stamps it on every later record
from that browser that arrives without one, before L0 looks. It is remembered whether or not
the report itself was in scope — a denied site is exactly the one that has to be.

The join is by browser family rather than by executable string, because the two halves spell
the browser differently: the extension can only say `chrome.exe`, from a user-agent string,
while a Mac names the foreground app by its bundle, `Google Chrome`. A join on the string
passes on Windows and captures the bank on every Mac, which is the platform with the shipped
build. The table is closed — it holds the four names the extension can report — so a site
learned from a browser is never stamped on an application.

What this does not close, and says so: a frame captured between a navigation and the
extension's report still carries the previous site. That is the extension's reporting latency,
and it closes on the report.

Three pipeline tests, each the claim it defends: the Windows path, the Mac bundle name, and
that one browser's site reaches neither an editor nor another browser. All three fail against
the old code — checked, by making the join a no-op.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
LNK merged commit 92ba36cd6f into main 2026-09-04 16:19:21 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
mikebrandon/here-telemetry-rnd!1
No description provided.