Skip to content

fix(privacy): the promises drifted away from the code - #539

Merged
runyourempire merged 2 commits into
mainfrom
fix/repo-audit-2026-08-28
Aug 28, 2026
Merged

fix(privacy): the promises drifted away from the code#539
runyourempire merged 2 commits into
mainfrom
fix/repo-audit-2026-08-28

Conversation

@runyourempire

Copy link
Copy Markdown
Collaborator

Findings from a repo-wide audit run today across six domains (release/distribution, security/privacy, scoring/ACE, frontend, data layer, CI/repo hygiene). This PR carries the subset that is small, verifiable, and closes something that is actually false today. The larger items are listed at the bottom rather than silently dropped.

The through-line: the code is careful, and several promises about the code had drifted away from it. Every fix here ships with a test or a gate, because in each case the defect survived precisely because nothing tested the claim.


Privacy — two things NETWORK.md promised that were not true

Git commit messages were being sent to a cloud LLM. build_rerank_context_summary put the five most recent commit messages into JudgeRequest.context_summary, which goes verbatim to whichever provider the user has configured. RerankConfig::enabled defaults to true, so one saved API key switched it on for every analysis cycle, and the titles_only privacy setting did not cover it (that applies to article content). NETWORK.md said: "Project files, source code, file contents, and git history never leave your machine."

Removed rather than truncated further — a commit subject is prose the developer wrote, and no length makes it not-git-history. The signal it carried is already supplied by dependencies, git-derived work topics, and declared interests: nouns, not the developer's own sentences.

New gate scripts/check-privacy-egress.cjs confines raw-content columns to the modules that mine and store them, with six tests covering the exact regression, a module nobody has thought of yet, and the escape hatch in both directions. Wired into validate and CI repo-guards. The real defect was that nothing tested the promise.

The disclosure list was wrong. NETWORK.md said the only locally-derived data sent anywhere was OSV package names. In fact npm, PyPI, crates.io, proxy.golang.org and the GitHub advisory API all receive your package names, GitHub search receives your languages, Reddit your subreddits, Stack Overflow your tags. Replaced with an exhaustive table, and the sharpest edge is called out explicitly: a private Go module path such as github.com/yourcompany/internal-service discloses your organisation and repository name to Google's module proxy.

Security

Any non-empty string in settings.json granted Signal, permanently. key_is_usable returned true for any key not starting with 4DA-, and it is the FAST PATH — so it short-circuited before the validation cache it claimed to rely on was ever consulted. {"tier":"signal","license_key":"x"} was a permanent paid tier: no signature, no network call, no expiry. The comment directly above it asserted that this hole was closed; the fix had only ever covered the 4DA- branch, and there was no test.

Now a Keygen-format key is usable only if the validation cache vouches for that key — matching SHA-256 hash, paid tier, inside the freshness window — via the is_cache_valid helper that already existed. The same hole in the backup-file recovery layer ("Keygen format — trusted") is closed the same way. Six assertions including a positive control, because a fix that always returned false would strand every real customer.

The SSRF guard was switched off by an attacker-controllable string. Three sites read if provider != "ollama" { validate_not_internal(url) }. provider is a value the frontend sets over IPC, so provider: "ollama" disabled validation for any base_url, cloud metadata endpoints included. Replaced with a host-based validate_llm_endpoint, which fixes the INV-032 false negative in the same change: a local LM Studio, llama.cpp or Jan server is exactly as safe as Ollama and was blocked purely for not being called "ollama". Private LAN and metadata addresses stay blocked — loopback is the user's own machine, 169.254.169.254 is not.

The secret scanner missed every key format the app accepts. /sk-[a-zA-Z0-9]{32,}/ stops at the first hyphen, so sk-ant-api03-... and sk-proj-... both sailed through while the audit reported "0 findings" across 1,958 files. Patterns moved to scripts/lib/secret-patterns.cjs (mirroring pii-hashes.cjs), broadened, and given a placeholder filter keyed on the matched text rather than the file path — a path exemption would blanket-excuse a real key dropped into a test file, which is one of the likelier ways a key leaks. Ten tests, asserting both directions. Broadening it immediately surfaced five live matches, all fixtures, which is how the filter came to exist.

Also: data/*.tmp is now gitignored. The final .json and .bak were; the .tmp they are written through was not, and settings.json.tmp holds the API key and licence key in plaintext for the width of a rename.

Release — two config lines that make a release possible at all

bundle.createUpdaterArtifacts was unset, and it defaults to false. Tauri therefore emitted no updater signature, latest.json was never produced, and release.yml hard-requires it before publishing — so the release stays a draft. The existing gate checked that the workflow asked for latest.json and never that the bundler was configured to produce it: text, not meaning.

desktop-template.desktop still registered x-scheme-handler/4da, four months after #491 renamed the scheme to fourda, because a custom desktopTemplate opts out of Tauri's generated entry and nothing in the repo referenced the file. Also StartupWMClass=4da against a fourda binary, and {{exec_dir}}/{{exec_name}} in the desktop action — variables Tauri's template schema does not define, so they rendered empty. Exec= now passes %U, without which a deep link arrives with no URL at all.

check-release-channel.cjs now covers all of it, with four negative tests.

Download-page copy. The live page advertises auto-updates the build cannot perform, and explains the SmartScreen prompt as "a new app still building reputation — not a warning about the binary." The shipped installer is unsigned; that is a materially different prompt. Corrected to say so plainly and to point at hash verification.

Frontend

Snooze was broken on every card, and told the user so in English. The frontend sends 'snooze' from two call sites; feedbackTypeMap was typed Record<string, string> and had no snooze key, so the lookup yielded undefined, a ! silenced it, the field was dropped from the IPC payload, and both backend commands rejected — one on an unknown action type, one on a missing required arg. The store then surfaced a hardcoded-English "Feedback not fully saved" toast to all 13 locales. That is issue #487, still live and louder than when it was filed.

Fixed end to end: a real BehaviorAction::Snooze (strength −0.4, half of Dismiss, mirroring the frontend's own −0.05 vs −0.10 grading — collapsing it into Dismiss would teach ACE that a deferral is a rejection), arms in both commands, and the map retyped to Record<FeedbackAction, string> so omitting a union member is a compile error rather than a runtime drop. Toast routed through i18n.

The crash screen's recovery button was invisible in light theme#fff on var(--color-border), which is #DDDAD2 in light: about 1.3:1. Tokenized, along with the wrapper.

Housekeeping

Four i18n scripts plus the CI validator iterated a coach namespace that no locale directory has ever contained, which is why i18n:status printed coach: undefined. Removed; the real set is ui / errors / signals.


Verification

  • cargo test --lib — 4654 passed, 0 failed
  • pnpm run test — full frontend suite green
  • pnpm run typecheck — 0 errors
  • pnpm run audit:public-ready — no findings, re-run after broadening the patterns
  • node --test on the three touched gate suites — 23 tests green, including 11 new negative tests
  • cargo fmt --check, cargo clippy --lib — clean

Deliberately NOT in this PR

Real findings from the same audit that need their own measured change, recorded here so they are not lost:

  • Windows code signing is dead — SSL.com eSigner rejects the credentials; the release workflow has 2 successes in 25 runs and last ran 68 days ago. Account work plus a dry-run on a throwaway tag.
  • The download page still serves v1.0.0 from 20 April, roughly 1,800 commits behind, and its release notes carry retired AD-030 claims.
  • Two CWD-dependent bugs (ace_context.rs:157, monitoring_briefing.rs:894) silently mis-weight an installed build; the scheduled refresh inherits C:\Windows\System32 as its working directory.
  • dependency_edges is an unbounded append log — 613,674 rows for 12,178 distinct tuples, 145 MB, +9 MB/day, and its only reader is a test. Needs schema 114.
  • office.rs runs two third-party parsers on hostile bytes with no catch_unwind, where pdf.rs has one; and no HTTP adapter caps response body size.
  • Entitlement revocation is unconsumed — refresh credentials are minted per customer and nothing reads them (PR feat(license): lease-model client — offline-verifiable short tokens w/ auto-refresh #361).
  • privacy.llm_content_level is documented in NETWORK.md but has no UI control and is honoured by 1 of 9 LLM egress sites.

A repo-wide audit across six domains. This carries what was small, verifiable,
and false today. Every fix ships with a test or a gate, because in every case
the defect survived precisely because nothing tested the claim.

PRIVACY — two things NETWORK.md promised that were not true.

Git commit messages were being sent to a cloud LLM. build_rerank_context_summary
put the five most recent commit messages into JudgeRequest.context_summary, which
goes verbatim to the configured provider. RerankConfig::enabled defaults to true,
so one saved API key switched it on for every analysis cycle, and titles_only did
not cover it — that applies to article content. NETWORK.md said "Project files,
source code, file contents, and git history never leave your machine." Removed
rather than truncated further: a commit subject is prose the developer wrote, and
no length makes it not-git-history. The signal it carried is already supplied by
dependencies, git-derived work TOPICS and declared interests.

New gate scripts/check-privacy-egress.cjs confines raw-content columns to the
modules that mine and store them. Six tests: the exact regression, a module
nobody has thought of yet, the allowlisted modules, documentation, and the escape
hatch in both directions. Wired into validate and CI repo-guards.

The disclosure list was also wrong — NETWORK.md claimed OSV package names were
the only locally-derived data sent anywhere. npm, PyPI, crates.io,
proxy.golang.org and the GitHub advisory API all receive package names; GitHub
search gets languages, Reddit subreddits, Stack Overflow tags. Replaced with an
exhaustive table, naming the sharpest edge: a private Go module path discloses
the org and repo name to Google's module proxy.

SECURITY.

Any non-empty string in settings.json granted Signal, permanently. key_is_usable
returned true for any key not starting with 4DA-, and it is the FAST PATH, so it
short-circuited before the validation cache it claimed to rely on was ever
consulted. No signature, no network call, no expiry. The comment directly above
it asserted the hole was closed; the fix had only covered the 4DA- branch, and
there was no test. A Keygen key is now usable only if the cache vouches for THAT
key — matching hash, paid tier, inside the freshness window — via the
is_cache_valid helper that already existed. Same hole closed in the backup-file
layer. Six assertions including a positive control, because a fix that always
returned false would strand every real customer.

The SSRF guard was switched off by an attacker-controllable string: three sites
read `if provider != "ollama"`, and provider is set by the frontend over IPC, so
provider "ollama" disabled validation for any base_url including cloud metadata.
Replaced with a host-based validate_llm_endpoint, which also fixes the INV-032
false negative — a local LM Studio or llama.cpp is as safe as Ollama and was
blocked for not being called "ollama". Private LAN and metadata stay blocked.

The secret scanner missed every key format the app accepts: the OpenAI pattern
stopped at the first hyphen, so the current Anthropic and OpenAI project formats
sailed through while the audit reported 0 findings over 1,958 files. Patterns
moved to scripts/lib/secret-patterns.cjs, broadened, plus a placeholder filter
keyed on the MATCHED TEXT not the file path — a path exemption would blanket-
excuse a real key in a test file. Ten tests, both directions. Broadening it
immediately surfaced five live matches, all fixtures, which is how the filter
came to exist. The fixtures themselves are assembled from fragments so no
grep-able key shape exists in the file.

data/*.tmp is now gitignored. The final .json and .bak were; the .tmp they are
written through was not, and settings.json.tmp holds the API key and licence key
in plaintext for the width of a rename.

RELEASE — two config lines that make a release possible at all.

bundle.createUpdaterArtifacts was unset and defaults to false, so Tauri emitted
no updater signature, latest.json was never produced, and release.yml requires it
before publishing. The gate checked that the workflow ASKED for latest.json and
never that the bundler could PRODUCE it: text, not meaning.

desktop-template.desktop still registered x-scheme-handler/4da four months after
the scheme was renamed to fourda, because a custom desktopTemplate opts out of
Tauri's generated entry and nothing referenced the file. Also StartupWMClass=4da
against a fourda binary, and template variables the schema does not define, which
rendered empty. Exec= now passes %U, without which a deep link arrives with no
URL. check-release-channel.cjs covers all of it, with four negative tests.

Download page: it advertised auto-updates the build cannot perform and explained
SmartScreen as "a new app still building reputation — not a warning about the
binary". The shipped installer is unsigned; that is a different prompt. Corrected.

FRONTEND.

Snooze was broken on every card and said so in English. The frontend sends
'snooze' from two call sites; feedbackTypeMap was Record<string, string> with no
snooze key, so the lookup yielded undefined, a non-null assertion silenced it,
the field was dropped from the payload, and both backend commands rejected. The
store then showed a hardcoded-English toast to all 13 locales. Issue #487, still
live. Fixed end to end with a real BehaviorAction::Snooze (strength -0.4, half of
Dismiss, mirroring the frontend's own -0.05 vs -0.10 — collapsing it into Dismiss
would teach ACE that a deferral is a rejection), arms in both commands, and the
map retyped to Record<FeedbackAction, string> so omitting a union member is a
compile error rather than a runtime drop. The toast reaches i18n through the
window handle utils/error-messages.ts already established, with a fallback that
still names the failing command — importing the i18n module directly runs
.use(initReactI18next) at import time and broke a suite that mocks react-i18next.

The crash screen's recovery button was invisible in light theme: white on
--color-border, which is #DDDAD2 there, about 1.3:1. Tokenized.

BUILD. chacha20 0.10.0 was yanked upstream, so cargo deny fails and takes the
Rust job — and therefore Validate Success, the sole required check — down on
EVERY open PR. Bumped to 0.10.2, NOTICE regenerated in the same commit because
the attribution gate is the only unfiltered leg of that same required check.

HOUSEKEEPING. Four i18n scripts and the CI validator iterated a coach namespace
no locale directory has ever contained — hence "coach: undefined" in i18n:status.

Verified: cargo test --lib 4654 passed 0 failed; pnpm run test 111 files / 1242
passed; typecheck 0 errors; lint 0 errors; audit:public-ready no findings;
23 gate tests green; cargo fmt --check and clippy --lib clean.
@runyourempire
runyourempire merged commit 0894a82 into main Aug 28, 2026
15 checks passed
@runyourempire
runyourempire deleted the fix/repo-audit-2026-08-28 branch August 28, 2026 07:34
runyourempire added a commit that referenced this pull request Aug 28, 2026
…he preflight (#545)

Today's dry run spent **25 minutes per platform** discovering this:

```
failed to decode secret key: failed to decode base64 secret key:
failed to decode base64 key: Invalid symbol 32, offset 0.
```

ASCII 32, at position 0 — `TAURI_SIGNING_PRIVATE_KEY` **begins with a
space**.

That secret was set on **2026-02-16**, is non-empty, and passed every
check this repository has. `gh secret list` shows it present. The
preflight's `require` only tests for emptiness. And it had never been
exercised at all, because `createUpdaterArtifacts` was unset until #539
— so nothing ever asked Tauri to decode it. The first release that got
far enough to need it would have died on it, after a full four-platform
compile.

## The change

The preflight exists precisely so a credential problem costs **seconds
instead of 45 minutes**. It was only checking that credentials are
*there*. Now it also checks they are *shaped* like credentials: no
leading or trailing whitespace, on every value, on every platform.

## Deliberately narrow

It does **not** validate content. That would mean reimplementing base64,
minisign and eSigner's auth inside a preflight — and a check that is
wrong about a *valid* credential is worse than no check at all, because
it blocks a good release.

Surrounding whitespace is unambiguous: no credential should ever carry
it, and a value pasted with a stray leading space is exactly the failure
that just cost an hour of runner time.

## Verified by execution, not by reading

The validator was extracted and run against fixtures:

| fixture | flagged? | why |
|---|---|---|
| leading space | ✅ yes | the actual bug |
| trailing space | ✅ yes | same class |
| leading tab | ✅ yes | same class |
| clean base64 key | ✅ no | must not block a good credential |
| **empty** | ✅ no | absence is `require`'s job, not this one's |
| **password with spaces *inside*** | ✅ no | legitimate; flagging it
would break a working release |

`bash -n` over the whole extracted preflight passes, and YAML re-parses.

That last pair matters. I caught a real bug in my own first attempt at
this — the shape loop landed *inside* the `shape()` function because my
anchor matched the wrong `esac`. YAML still parsed cleanly, because it
is all one block scalar. Only executing the bash found it.

## Not included

An `xcrun notarytool history` probe, which would have caught the
**other** credential failure this dry run found — Apple rejecting
notarization with *"a required agreement is missing or has expired"*,
again after a 25-minute macOS build.

It is the obvious next step and worth doing. But it cannot be tested
from this machine, and the release path is the wrong place to put code I
have only read. Recommended as a follow-up rather than shipped blind.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant