From 8d85fccb4b51e29aa4e6e4397b2cc494f25073ea Mon Sep 17 00:00:00 2001 From: blessdyb Date: Sat, 17 Oct 2026 23:06:26 -0700 Subject: [PATCH 1/5] Release Flowlight 0.15.0 Co-Authored-By: Claude Code --- docs/404.html | 2 +- docs/about/index.html | 2 +- docs/de/about/index.html | 2 +- docs/de/docs/index.html | 4 ++-- docs/de/index.html | 6 +++--- docs/de/privacy/index.html | 2 +- docs/de/threat-model/index.html | 2 +- docs/docs/index.html | 4 ++-- docs/es/about/index.html | 2 +- docs/es/docs/index.html | 4 ++-- docs/es/index.html | 6 +++--- docs/es/privacy/index.html | 2 +- docs/es/threat-model/index.html | 2 +- docs/fr/about/index.html | 2 +- docs/fr/docs/index.html | 4 ++-- docs/fr/index.html | 6 +++--- docs/fr/privacy/index.html | 2 +- docs/fr/threat-model/index.html | 2 +- docs/index.html | 6 +++--- docs/it/about/index.html | 2 +- docs/it/docs/index.html | 4 ++-- docs/it/index.html | 6 +++--- docs/it/privacy/index.html | 2 +- docs/it/threat-model/index.html | 2 +- docs/ja/about/index.html | 2 +- docs/ja/docs/index.html | 4 ++-- docs/ja/index.html | 6 +++--- docs/ja/privacy/index.html | 2 +- docs/ja/threat-model/index.html | 2 +- docs/ko/about/index.html | 2 +- docs/ko/docs/index.html | 4 ++-- docs/ko/index.html | 6 +++--- docs/ko/privacy/index.html | 2 +- docs/ko/threat-model/index.html | 2 +- docs/llms-full.txt | 25 ++++++++++++++++++++++--- docs/llms.txt | 2 +- docs/plugins/index.html | 2 +- docs/privacy/index.html | 2 +- docs/pt-PT/about/index.html | 2 +- docs/pt-PT/docs/index.html | 4 ++-- docs/pt-PT/index.html | 6 +++--- docs/pt-PT/privacy/index.html | 2 +- docs/pt-PT/threat-model/index.html | 2 +- docs/releases/index.html | 22 ++++++++++++++++++++-- docs/threat-model/index.html | 2 +- docs/zh-Hans/about/index.html | 2 +- docs/zh-Hans/docs/index.html | 4 ++-- docs/zh-Hans/index.html | 6 +++--- docs/zh-Hans/privacy/index.html | 2 +- docs/zh-Hans/threat-model/index.html | 2 +- docs/zh-Hant/about/index.html | 2 +- docs/zh-Hant/docs/index.html | 4 ++-- docs/zh-Hant/index.html | 6 +++--- docs/zh-Hant/privacy/index.html | 2 +- docs/zh-Hant/threat-model/index.html | 2 +- project.yml | 4 ++-- site/pages/releases.html | 20 +++++++++++++++++++- 57 files changed, 146 insertions(+), 91 deletions(-) diff --git a/docs/404.html b/docs/404.html index ed5c704e..6f4b3ab2 100644 --- a/docs/404.html +++ b/docs/404.html @@ -86,7 +86,7 @@

That page isn't here

Try the home page,

diff --git a/docs/about/index.html b/docs/about/index.html index a38fa366..5c366412 100644 --- a/docs/about/index.html +++ b/docs/about/index.html @@ -153,7 +153,7 @@

Thanks

diff --git a/docs/de/about/index.html b/docs/de/about/index.html index 0f988be2..6808ea58 100644 --- a/docs/de/about/index.html +++ b/docs/de/about/index.html @@ -152,7 +152,7 @@

Dank

diff --git a/docs/de/docs/index.html b/docs/de/docs/index.html index 4461004f..e0428011 100644 --- a/docs/de/docs/index.html +++ b/docs/de/docs/index.html @@ -76,7 +76,7 @@

Dokumentation

Flowlight benutzen

-

Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.14.5, macOS 15 oder neuer.

+

Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.15.0, macOS 15 oder neuer.

diff --git a/docs/de/index.html b/docs/de/index.html index c92f92ee..7936bf72 100644 --- a/docs/de/index.html +++ b/docs/de/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

Sieh, was deine Apps im Netz tun.
brew install --cask xinbetween/tap/flowlight

-

v0.14.5macOS 15+UniversalGPL-3.0Keine Telemetrie

+

v0.15.0macOS 15+UniversalGPL-3.0Keine Telemetrie

@@ -571,7 +571,7 @@

Wisse, was deinen Mac verlässt.

diff --git a/docs/de/privacy/index.html b/docs/de/privacy/index.html index 5bd44a43..9eb30c0d 100644 --- a/docs/de/privacy/index.html +++ b/docs/de/privacy/index.html @@ -188,7 +188,7 @@

Kontakt

diff --git a/docs/de/threat-model/index.html b/docs/de/threat-model/index.html index 356b430d..a0f6e895 100644 --- a/docs/de/threat-model/index.html +++ b/docs/de/threat-model/index.html @@ -228,7 +228,7 @@

Eine Schwachstelle melden

diff --git a/docs/docs/index.html b/docs/docs/index.html index c311e756..b6394a41 100644 --- a/docs/docs/index.html +++ b/docs/docs/index.html @@ -76,7 +76,7 @@

Documentation

Using Flowlight

-

Everything from the first launch to tuning the agent rules. Flowlight 0.14.5, macOS 15 or later.

+

Everything from the first launch to tuning the agent rules. Flowlight 0.15.0, macOS 15 or later.

diff --git a/docs/es/about/index.html b/docs/es/about/index.html index f92509b7..97f565d1 100644 --- a/docs/es/about/index.html +++ b/docs/es/about/index.html @@ -152,7 +152,7 @@

Agradecimientos

diff --git a/docs/es/docs/index.html b/docs/es/docs/index.html index 5e60ca7a..29d750cf 100644 --- a/docs/es/docs/index.html +++ b/docs/es/docs/index.html @@ -76,7 +76,7 @@

Documentación

Usar Flowlight

-

Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.14.5, macOS 15 o posterior.

+

Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.15.0, macOS 15 o posterior.

diff --git a/docs/es/index.html b/docs/es/index.html index 018fc274..d49e808b 100644 --- a/docs/es/index.html +++ b/docs/es/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

Observa la actividad de red de tus apps.

brew install --cask xinbetween/tap/flowlight

-

v0.14.5macOS 15+UniversalGPL-3.0Sin telemetría

+

v0.15.0macOS 15+UniversalGPL-3.0Sin telemetría

@@ -571,7 +571,7 @@

Ten claro qué sale de tu Mac.

diff --git a/docs/es/privacy/index.html b/docs/es/privacy/index.html index 323e53c3..228cb48d 100644 --- a/docs/es/privacy/index.html +++ b/docs/es/privacy/index.html @@ -188,7 +188,7 @@

Contacto

diff --git a/docs/es/threat-model/index.html b/docs/es/threat-model/index.html index f9a4a936..6cef695b 100644 --- a/docs/es/threat-model/index.html +++ b/docs/es/threat-model/index.html @@ -223,7 +223,7 @@

Informar de una vulnerabilidad

diff --git a/docs/fr/about/index.html b/docs/fr/about/index.html index 6b50674b..bac0cf2a 100644 --- a/docs/fr/about/index.html +++ b/docs/fr/about/index.html @@ -152,7 +152,7 @@

Remerciements

diff --git a/docs/fr/docs/index.html b/docs/fr/docs/index.html index 4d7507e3..25e3f2d5 100644 --- a/docs/fr/docs/index.html +++ b/docs/fr/docs/index.html @@ -76,7 +76,7 @@

Documentation

Utiliser Flowlight

-

Tout, du premier lancement au réglage des règles des agents. Flowlight 0.14.5, macOS 15 ou version ultérieure.

+

Tout, du premier lancement au réglage des règles des agents. Flowlight 0.15.0, macOS 15 ou version ultérieure.

diff --git a/docs/fr/index.html b/docs/fr/index.html index 1f984ac0..cf0c4e6d 100644 --- a/docs/fr/index.html +++ b/docs/fr/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

Voyez l’activité réseau de vos apps.

brew install --cask xinbetween/tap/flowlight

-

v0.14.5macOS 15+UniverselGPL-3.0Sans télémétrie

+

v0.15.0macOS 15+UniverselGPL-3.0Sans télémétrie

@@ -571,7 +571,7 @@

Sachez ce qui quitte votre Mac.

diff --git a/docs/fr/privacy/index.html b/docs/fr/privacy/index.html index bca75118..644ea7a6 100644 --- a/docs/fr/privacy/index.html +++ b/docs/fr/privacy/index.html @@ -188,7 +188,7 @@

Contact

diff --git a/docs/fr/threat-model/index.html b/docs/fr/threat-model/index.html index a2e218d0..bf75af78 100644 --- a/docs/fr/threat-model/index.html +++ b/docs/fr/threat-model/index.html @@ -226,7 +226,7 @@

Signaler une vulnérabilité

diff --git a/docs/index.html b/docs/index.html index e2a4cbe9..7f5f7e26 100644 --- a/docs/index.html +++ b/docs/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

See your apps' network activity.
brew install --cask xinbetween/tap/flowlight

-

v0.14.5macOS 15+UniversalGPL-3.0No telemetry

+

v0.15.0macOS 15+UniversalGPL-3.0No telemetry

@@ -592,7 +592,7 @@

Know what leaves your Mac.

diff --git a/docs/it/about/index.html b/docs/it/about/index.html index 8dea6237..4e8d6d9d 100644 --- a/docs/it/about/index.html +++ b/docs/it/about/index.html @@ -152,7 +152,7 @@

Ringraziamenti

diff --git a/docs/it/docs/index.html b/docs/it/docs/index.html index f6be250c..f0aa3c17 100644 --- a/docs/it/docs/index.html +++ b/docs/it/docs/index.html @@ -76,7 +76,7 @@

Documentazione

Usare Flowlight

-

Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.14.5, macOS 15 o successivo.

+

Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.15.0, macOS 15 o successivo.

diff --git a/docs/it/index.html b/docs/it/index.html index a4a7539a..1384eee2 100644 --- a/docs/it/index.html +++ b/docs/it/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

Vedi la rete delle tue app.
Ca Metti una stella su GitHub

brew install --cask xinbetween/tap/flowlight

-

v0.14.5macOS 15+UniversaleGPL-3.0Nessuna telemetria

+

v0.15.0macOS 15+UniversaleGPL-3.0Nessuna telemetria

@@ -571,7 +571,7 @@

Sappi che cosa esce dal tuo Mac.

diff --git a/docs/it/privacy/index.html b/docs/it/privacy/index.html index 359df4d7..2766b308 100644 --- a/docs/it/privacy/index.html +++ b/docs/it/privacy/index.html @@ -188,7 +188,7 @@

Contatti

diff --git a/docs/it/threat-model/index.html b/docs/it/threat-model/index.html index 23d6da4a..73492b36 100644 --- a/docs/it/threat-model/index.html +++ b/docs/it/threat-model/index.html @@ -224,7 +224,7 @@

Segnalare una vulnerabilità

diff --git a/docs/ja/about/index.html b/docs/ja/about/index.html index 5af756e8..a4157790 100644 --- a/docs/ja/about/index.html +++ b/docs/ja/about/index.html @@ -153,7 +153,7 @@

謝辞

diff --git a/docs/ja/docs/index.html b/docs/ja/docs/index.html index 4c1f69cb..46d08657 100644 --- a/docs/ja/docs/index.html +++ b/docs/ja/docs/index.html @@ -76,7 +76,7 @@

ドキュメント

Flowlight の使い方

-

初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.14.5、macOS 15 以降。

+

初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.15.0、macOS 15 以降。

diff --git a/docs/ja/index.html b/docs/ja/index.html index 6551bfcf..4d51fe7b 100644 --- a/docs/ja/index.html +++ b/docs/ja/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

アプリの通信が見える。
brew install --cask xinbetween/tap/flowlight

-

v0.14.5macOS 15+ユニバーサルGPL-3.0テレメトリなし

+

v0.15.0macOS 15+ユニバーサルGPL-3.0テレメトリなし

@@ -570,7 +570,7 @@

Mac から何が出ていくのかを知る。

diff --git a/docs/ja/privacy/index.html b/docs/ja/privacy/index.html index e7b1924c..c50ff695 100644 --- a/docs/ja/privacy/index.html +++ b/docs/ja/privacy/index.html @@ -186,7 +186,7 @@

連絡先

diff --git a/docs/ja/threat-model/index.html b/docs/ja/threat-model/index.html index 04db3de1..43a0fe32 100644 --- a/docs/ja/threat-model/index.html +++ b/docs/ja/threat-model/index.html @@ -225,7 +225,7 @@

脆弱性を報告する

diff --git a/docs/ko/about/index.html b/docs/ko/about/index.html index 30cbf96e..b7c8bb1b 100644 --- a/docs/ko/about/index.html +++ b/docs/ko/about/index.html @@ -153,7 +153,7 @@

감사

diff --git a/docs/ko/docs/index.html b/docs/ko/docs/index.html index c0cd067a..a0a04598 100644 --- a/docs/ko/docs/index.html +++ b/docs/ko/docs/index.html @@ -76,7 +76,7 @@

문서

Flowlight 사용하기

-

첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.14.5, macOS 15 이상.

+

첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.15.0, macOS 15 이상.

diff --git a/docs/ko/index.html b/docs/ko/index.html index 43e312df..2194f32d 100644 --- a/docs/ko/index.html +++ b/docs/ko/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

앱이 무엇을 하는지 봅니다.

brew install --cask xinbetween/tap/flowlight

-

v0.14.5macOS 15+유니버설GPL-3.0텔레메트리 없음

+

v0.15.0macOS 15+유니버설GPL-3.0텔레메트리 없음

@@ -571,7 +571,7 @@

내 Mac에서 무엇이 나가는지 아세요.

diff --git a/docs/ko/privacy/index.html b/docs/ko/privacy/index.html index e864c83f..92c37acb 100644 --- a/docs/ko/privacy/index.html +++ b/docs/ko/privacy/index.html @@ -189,7 +189,7 @@

문의

diff --git a/docs/ko/threat-model/index.html b/docs/ko/threat-model/index.html index 886bf819..f2458717 100644 --- a/docs/ko/threat-model/index.html +++ b/docs/ko/threat-model/index.html @@ -223,7 +223,7 @@

취약점 신고하기

diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 93d664d8..56e119ba 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -62,7 +62,7 @@ Documentation Using Flowlight - Everything from the first launch to tuning the agent rules. Flowlight 0.14.5, macOS 15 or later. + Everything from the first launch to tuning the agent rules. Flowlight 0.15.0, macOS 15 or later. On this page @@ -887,7 +887,7 @@ Understand your AI agents. brew install --cask xinbetween/tap/flowlightCopy - v0.14.5macOS 15+UniversalGPL-3.0No telemetry + v0.15.0macOS 15+UniversalGPL-3.0No telemetry connectionslive @@ -1617,9 +1617,28 @@ Releases Downloads, checksums and full notes for each version are on GitHub Releases. - 0.14.5 + 0.15.0 October 17, 2026Latest + Block known ad and tracker destinations across apps. Three official managed packs cover + advertising, cross-site tracking, and privacy/telemetry hosts. They ship disabled, require explicit review, + and can apply to every app or only selected apps. + + Keep blocking explainable and reversible. Preview seven days of retained traffic before + enabling a pack, review its built-in destinations and provenance, inspect aggregated activity, add scoped + per-app exceptions, or use an ordinary allow rule as the universal escape hatch. + + Fail open with atomic, version-bound enforcement. Flowlight validates and transfers one + complete pack generation to the Network Extension, binds consent to the exact reviewed content, preserves + ordinary-rule and essential-system precedence, and never treats a partial or stale update as active. + + Set honest network-only expectations. Hostname blocking cannot remove first-party ads, + page gaps, browser overlays, scripted popups or native app windows; those need the later browser extension. + ECH, QUIC, encrypted DNS, direct IPs, shared CDNs and unavailable hostnames can also limit coverage. + + 0.14.5 +October 17, 2026 + Review plugin actions in Flowlight's existing editors. Plugin findings can now suggest guardrails, network rules, mock responses, request rewrites and response transforms as typed, inert actions. Inspect opens the matching editor with bounded defaults, and nothing changes until the user reviews and saves it. diff --git a/docs/llms.txt b/docs/llms.txt index 86ab145d..bc4480cd 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -1,6 +1,6 @@ # Flowlight -> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.14.5. +> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.15.0. - [Download Flowlight.dmg](https://github.com/xinbetween/flowlight/releases/latest/download/Flowlight.dmg) - [Source code](https://github.com/xinbetween/flowlight) diff --git a/docs/plugins/index.html b/docs/plugins/index.html index f79a6096..3267f5d1 100644 --- a/docs/plugins/index.html +++ b/docs/plugins/index.html @@ -211,7 +211,7 @@

Review checklist

diff --git a/docs/privacy/index.html b/docs/privacy/index.html index a45c0bdd..1c45a745 100644 --- a/docs/privacy/index.html +++ b/docs/privacy/index.html @@ -195,7 +195,7 @@

Contact

diff --git a/docs/pt-PT/about/index.html b/docs/pt-PT/about/index.html index 51095b6b..be10595b 100644 --- a/docs/pt-PT/about/index.html +++ b/docs/pt-PT/about/index.html @@ -152,7 +152,7 @@

Agradecimentos

diff --git a/docs/pt-PT/docs/index.html b/docs/pt-PT/docs/index.html index e786828c..20bfd264 100644 --- a/docs/pt-PT/docs/index.html +++ b/docs/pt-PT/docs/index.html @@ -76,7 +76,7 @@

Documentação

Usar o Flowlight

-

Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.14.5, macOS 15 ou posterior.

+

Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.15.0, macOS 15 ou posterior.

diff --git a/docs/pt-PT/index.html b/docs/pt-PT/index.html index 5d03fbd0..6169e0aa 100644 --- a/docs/pt-PT/index.html +++ b/docs/pt-PT/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

Veja a atividade de rede das suas apps.

brew install --cask xinbetween/tap/flowlight

-

v0.14.5macOS 15+UniversalGPL-3.0Sem telemetria

+

v0.15.0macOS 15+UniversalGPL-3.0Sem telemetria

@@ -571,7 +571,7 @@

Saiba o que sai do seu Mac.

diff --git a/docs/pt-PT/privacy/index.html b/docs/pt-PT/privacy/index.html index dcc3a722..d042d6ff 100644 --- a/docs/pt-PT/privacy/index.html +++ b/docs/pt-PT/privacy/index.html @@ -188,7 +188,7 @@

Contacto

diff --git a/docs/pt-PT/threat-model/index.html b/docs/pt-PT/threat-model/index.html index 40df0438..188600cc 100644 --- a/docs/pt-PT/threat-model/index.html +++ b/docs/pt-PT/threat-model/index.html @@ -225,7 +225,7 @@

Comunicar uma vulnerabilidade

diff --git a/docs/releases/index.html b/docs/releases/index.html index 204293f7..c721081e 100644 --- a/docs/releases/index.html +++ b/docs/releases/index.html @@ -61,7 +61,25 @@

What's new

-

0.14.5

Latest
+

0.15.0

Latest
+
    +
  • Block known ad and tracker destinations across apps. Three official managed packs cover + advertising, cross-site tracking, and privacy/telemetry hosts. They ship disabled, require explicit review, + and can apply to every app or only selected apps.
  • +
  • Keep blocking explainable and reversible. Preview seven days of retained traffic before + enabling a pack, review its built-in destinations and provenance, inspect aggregated activity, add scoped + per-app exceptions, or use an ordinary allow rule as the universal escape hatch.
  • +
  • Fail open with atomic, version-bound enforcement. Flowlight validates and transfers one + complete pack generation to the Network Extension, binds consent to the exact reviewed content, preserves + ordinary-rule and essential-system precedence, and never treats a partial or stale update as active.
  • +
  • Set honest network-only expectations. Hostname blocking cannot remove first-party ads, + page gaps, browser overlays, scripted popups or native app windows; those need the later browser extension. + ECH, QUIC, encrypted DNS, direct IPs, shared CDNs and unavailable hostnames can also limit coverage.
  • +
+
+ +
+

0.14.5

  • Review plugin actions in Flowlight's existing editors. Plugin findings can now suggest guardrails, network rules, mock responses, request rewrites and response transforms as typed, inert actions. Inspect opens the @@ -1215,7 +1233,7 @@

    What's new

diff --git a/docs/threat-model/index.html b/docs/threat-model/index.html index 60ff492b..f9395ed0 100644 --- a/docs/threat-model/index.html +++ b/docs/threat-model/index.html @@ -224,7 +224,7 @@

Reporting a vulnerability

diff --git a/docs/zh-Hans/about/index.html b/docs/zh-Hans/about/index.html index 8a2688b7..83befd0a 100644 --- a/docs/zh-Hans/about/index.html +++ b/docs/zh-Hans/about/index.html @@ -152,7 +152,7 @@

致谢

diff --git a/docs/zh-Hans/docs/index.html b/docs/zh-Hans/docs/index.html index 3db9cbfa..b45fce5c 100644 --- a/docs/zh-Hans/docs/index.html +++ b/docs/zh-Hans/docs/index.html @@ -76,7 +76,7 @@

文档

使用 Flowlight

-

从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.14.5,macOS 15 或更高版本。

+

从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.15.0,macOS 15 或更高版本。

diff --git a/docs/zh-Hans/index.html b/docs/zh-Hans/index.html index 7b1e660f..06504594 100644 --- a/docs/zh-Hans/index.html +++ b/docs/zh-Hans/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

看清应用的网络活动。

brew install --cask xinbetween/tap/flowlight

-

v0.14.5macOS 15+通用架构GPL-3.0无遥测

+

v0.15.0macOS 15+通用架构GPL-3.0无遥测

@@ -568,7 +568,7 @@

知道什么离开了你的 Mac。

diff --git a/docs/zh-Hans/privacy/index.html b/docs/zh-Hans/privacy/index.html index 5978d342..aee7d31e 100644 --- a/docs/zh-Hans/privacy/index.html +++ b/docs/zh-Hans/privacy/index.html @@ -186,7 +186,7 @@

联系方式

diff --git a/docs/zh-Hans/threat-model/index.html b/docs/zh-Hans/threat-model/index.html index 58c4c32d..1d0afd87 100644 --- a/docs/zh-Hans/threat-model/index.html +++ b/docs/zh-Hans/threat-model/index.html @@ -212,7 +212,7 @@

报告漏洞

diff --git a/docs/zh-Hant/about/index.html b/docs/zh-Hant/about/index.html index 77e80c69..cb3ba5d0 100644 --- a/docs/zh-Hant/about/index.html +++ b/docs/zh-Hant/about/index.html @@ -152,7 +152,7 @@

致謝

diff --git a/docs/zh-Hant/docs/index.html b/docs/zh-Hant/docs/index.html index bdd338f5..d34700f1 100644 --- a/docs/zh-Hant/docs/index.html +++ b/docs/zh-Hant/docs/index.html @@ -76,7 +76,7 @@

說明文件

使用 Flowlight

-

從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.14.5,macOS 15 或以上版本。

+

從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.15.0,macOS 15 或以上版本。

diff --git a/docs/zh-Hant/index.html b/docs/zh-Hant/index.html index b06cdaf4..047c262c 100644 --- a/docs/zh-Hant/index.html +++ b/docs/zh-Hant/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

看見每個 App 的網路活動。
brew install --cask xinbetween/tap/flowlight

-

v0.14.5macOS 15+通用架構GPL-3.0無遙測

+

v0.15.0macOS 15+通用架構GPL-3.0無遙測

@@ -569,7 +569,7 @@

知道有什麼離開了你的 Mac。

diff --git a/docs/zh-Hant/privacy/index.html b/docs/zh-Hant/privacy/index.html index 97a5c639..fa1253a2 100644 --- a/docs/zh-Hant/privacy/index.html +++ b/docs/zh-Hant/privacy/index.html @@ -186,7 +186,7 @@

聯絡

diff --git a/docs/zh-Hant/threat-model/index.html b/docs/zh-Hant/threat-model/index.html index d440a13b..97fa0803 100644 --- a/docs/zh-Hant/threat-model/index.html +++ b/docs/zh-Hant/threat-model/index.html @@ -213,7 +213,7 @@

回報漏洞

diff --git a/project.yml b/project.yml index a20d9f87..8325e17f 100644 --- a/project.yml +++ b/project.yml @@ -11,8 +11,8 @@ settings: DEVELOPMENT_TEAM: "" CODE_SIGN_STYLE: Automatic ENABLE_HARDENED_RUNTIME: YES - MARKETING_VERSION: "0.14.5" - CURRENT_PROJECT_VERSION: "35" + MARKETING_VERSION: "0.15.0" + CURRENT_PROJECT_VERSION: "36" targets: Flowlight: type: application diff --git a/site/pages/releases.html b/site/pages/releases.html index 337d1de7..56d6bdd8 100644 --- a/site/pages/releases.html +++ b/site/pages/releases.html @@ -13,7 +13,25 @@

What's new

-

0.14.5

Latest
+

0.15.0

Latest
+
    +
  • Block known ad and tracker destinations across apps. Three official managed packs cover + advertising, cross-site tracking, and privacy/telemetry hosts. They ship disabled, require explicit review, + and can apply to every app or only selected apps.
  • +
  • Keep blocking explainable and reversible. Preview seven days of retained traffic before + enabling a pack, review its built-in destinations and provenance, inspect aggregated activity, add scoped + per-app exceptions, or use an ordinary allow rule as the universal escape hatch.
  • +
  • Fail open with atomic, version-bound enforcement. Flowlight validates and transfers one + complete pack generation to the Network Extension, binds consent to the exact reviewed content, preserves + ordinary-rule and essential-system precedence, and never treats a partial or stale update as active.
  • +
  • Set honest network-only expectations. Hostname blocking cannot remove first-party ads, + page gaps, browser overlays, scripted popups or native app windows; those need the later browser extension. + ECH, QUIC, encrypted DNS, direct IPs, shared CDNs and unavailable hostnames can also limit coverage.
  • +
+
+ +
+

0.14.5

  • Review plugin actions in Flowlight's existing editors. Plugin findings can now suggest guardrails, network rules, mock responses, request rewrites and response transforms as typed, inert actions. Inspect opens the From 76276bf48270dddcb5c8d21d8205e1995978fc6f Mon Sep 17 00:00:00 2001 From: blessdyb Date: Sun, 18 Oct 2026 00:50:58 -0700 Subject: [PATCH 2/5] Release Flowlight 0.15.1 Keep managed-pack edits local until an explicit save, route app-field submission to Add, and refresh cards from committed pack state. Co-Authored-By: Claude Code --- Flowlight/Analysis/RulePackStore.swift | 21 ++-- Flowlight/UI/RulePackEditorDraft.swift | 63 ++++++++++++ Flowlight/UI/RulePacksView.swift | 137 ++++++++++++++----------- FlowlightTests/RulePackTests.swift | 104 +++++++++++++++++++ docs/404.html | 2 +- docs/about/index.html | 2 +- docs/de/about/index.html | 2 +- docs/de/docs/index.html | 4 +- docs/de/index.html | 6 +- docs/de/privacy/index.html | 2 +- docs/de/threat-model/index.html | 2 +- docs/docs/index.html | 4 +- docs/es/about/index.html | 2 +- docs/es/docs/index.html | 4 +- docs/es/index.html | 6 +- docs/es/privacy/index.html | 2 +- docs/es/threat-model/index.html | 2 +- docs/fr/about/index.html | 2 +- docs/fr/docs/index.html | 4 +- docs/fr/index.html | 6 +- docs/fr/privacy/index.html | 2 +- docs/fr/threat-model/index.html | 2 +- docs/index.html | 6 +- docs/it/about/index.html | 2 +- docs/it/docs/index.html | 4 +- docs/it/index.html | 6 +- docs/it/privacy/index.html | 2 +- docs/it/threat-model/index.html | 2 +- docs/ja/about/index.html | 2 +- docs/ja/docs/index.html | 4 +- docs/ja/index.html | 6 +- docs/ja/privacy/index.html | 2 +- docs/ja/threat-model/index.html | 2 +- docs/ko/about/index.html | 2 +- docs/ko/docs/index.html | 4 +- docs/ko/index.html | 6 +- docs/ko/privacy/index.html | 2 +- docs/ko/threat-model/index.html | 2 +- docs/llms-full.txt | 18 +++- docs/llms.txt | 2 +- docs/plugins/index.html | 2 +- docs/privacy/index.html | 2 +- docs/pt-PT/about/index.html | 2 +- docs/pt-PT/docs/index.html | 4 +- docs/pt-PT/index.html | 6 +- docs/pt-PT/privacy/index.html | 2 +- docs/pt-PT/threat-model/index.html | 2 +- docs/releases/index.html | 18 +++- docs/sitemap.xml | 2 +- docs/threat-model/index.html | 2 +- docs/zh-Hans/about/index.html | 2 +- docs/zh-Hans/docs/index.html | 4 +- docs/zh-Hans/index.html | 6 +- docs/zh-Hans/privacy/index.html | 2 +- docs/zh-Hans/threat-model/index.html | 2 +- docs/zh-Hant/about/index.html | 2 +- docs/zh-Hant/docs/index.html | 4 +- docs/zh-Hant/index.html | 6 +- docs/zh-Hant/privacy/index.html | 2 +- docs/zh-Hant/threat-model/index.html | 2 +- project.yml | 4 +- site/pages/releases.html | 14 ++- 62 files changed, 390 insertions(+), 157 deletions(-) create mode 100644 Flowlight/UI/RulePackEditorDraft.swift diff --git a/Flowlight/Analysis/RulePackStore.swift b/Flowlight/Analysis/RulePackStore.swift index cd95e6c0..718a29c7 100644 --- a/Flowlight/Analysis/RulePackStore.swift +++ b/Flowlight/Analysis/RulePackStore.swift @@ -72,8 +72,11 @@ final class RulePackStore: ObservableObject { } } - func save(_ configuration: RulePackConfiguration) { - guard definitions.contains(where: { $0.id == configuration.packID }) else { return } + func save(_ configuration: RulePackConfiguration, completion: ((Bool) -> Void)? = nil) { + guard definitions.contains(where: { $0.id == configuration.packID }) else { + completion?(false) + return + } var cleaned = configuration if cleaned.enabled, let definition = definitions.first(where: { $0.id == cleaned.packID }) { cleaned.consentedVersion = definition.manifest.version @@ -83,15 +86,18 @@ final class RulePackStore: ObservableObject { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty })).sorted() if cleaned.scope == .selectedApps && cleaned.apps.isEmpty { cleaned.enabled = false } - persist({ try $0.saveRulePackConfiguration(cleaned) }) { [weak self] in - guard let self else { return } + persist({ try $0.saveRulePackConfiguration(cleaned) }, then: { [weak self] in + guard let self else { completion?(false); return } if let index = self.configurations.firstIndex(where: { $0.packID == cleaned.packID }) { self.configurations[index] = cleaned } else { self.configurations.append(cleaned) } self.publish() - } + completion?(true) + }, onFailure: { + completion?(false) + }) } func setEnabled(_ packID: String, _ enabled: Bool) { @@ -171,9 +177,11 @@ final class RulePackStore: ObservableObject { } private func persist(_ write: @escaping (TrafficDatabase) throws -> Void, - then commit: @escaping @MainActor () -> Void) { + then commit: @escaping @MainActor () -> Void, + onFailure: (@MainActor () -> Void)? = nil) { guard let db else { status = .failed("Rule pack storage is unavailable.") + onFailure?() return } db.queue.async { [weak self, weak db] in @@ -184,6 +192,7 @@ final class RulePackStore: ObservableObject { } catch { Task { @MainActor in self?.status = .failed("Could not save the rule pack change: \(error.localizedDescription)") + onFailure?() } } } diff --git a/Flowlight/UI/RulePackEditorDraft.swift b/Flowlight/UI/RulePackEditorDraft.swift new file mode 100644 index 00000000..279ccca3 --- /dev/null +++ b/Flowlight/UI/RulePackEditorDraft.swift @@ -0,0 +1,63 @@ +import Foundation + +/// The managed-pack editor is a draft until an explicit Save succeeds. Keeping these transitions outside SwiftUI +/// makes keyboard actions and sheet dismissal incapable of changing enforcement by accident. +struct RulePackEditorDraft { + enum Intent { + case review + case activate + } + + enum AppSubmission: Equatable { + case ignored + case added(String) + } + + enum SaveAction: Equatable { + case commit(RulePackConfiguration) + case confirmBroadening + } + + let originalConfiguration: RulePackConfiguration + var configuration: RulePackConfiguration + var typedApp = "" + private(set) var saveAction: SaveAction? + + init(saved: RulePackConfiguration, intent: Intent) { + originalConfiguration = saved + configuration = saved + if intent == .activate { configuration.enabled = true } + } + + var canSave: Bool { + configuration.scope != .selectedApps || !configuration.apps.isEmpty + } + + mutating func submitApp(installed: [InstalledApps.App]) -> AppSubmission { + addApp(typedApp, installed: installed) + } + + mutating func addApp(_ value: String, installed: [InstalledApps.App]) -> AppSubmission { + guard let target = InstalledApps.target(forTyped: value, apps: installed) else { return .ignored } + if !configuration.apps.contains(where: { $0.caseInsensitiveCompare(target.value) == .orderedSame }) { + configuration.apps.append(target.value) + configuration.apps.sort() + } + typedApp = "" + return .added(target.value) + } + + mutating func requestSave() -> SaveAction? { + guard canSave else { saveAction = nil; return nil } + if originalConfiguration.enabled, + originalConfiguration.scope == .selectedApps, + configuration.scope == .allApps { + saveAction = .confirmBroadening + } else { + saveAction = .commit(configuration) + } + return saveAction + } + + mutating func clearSaveAction() { saveAction = nil } +} diff --git a/Flowlight/UI/RulePacksView.swift b/Flowlight/UI/RulePacksView.swift index 08995851..8a9d60ee 100644 --- a/Flowlight/UI/RulePacksView.swift +++ b/Flowlight/UI/RulePacksView.swift @@ -2,25 +2,43 @@ import SwiftUI struct RulePacksView: View { @EnvironmentObject var monitor: TrafficMonitor - @State private var editor: RulePackDefinition? + + var body: some View { + RulePacksContent(packs: monitor.rulePacks) + } +} + +private struct RulePackEditorRequest: Identifiable { + let pack: RulePackDefinition + let saved: RulePackConfiguration + let intent: RulePackEditorDraft.Intent + var id: String { pack.id } +} + +private struct RulePacksContent: View { + @EnvironmentObject var monitor: TrafficMonitor + @ObservedObject var packs: RulePackStore + @State private var editor: RulePackEditorRequest? var body: some View { ScrollView { LazyVStack(alignment: .leading, spacing: 10) { explanation - if monitor.rulePacks.deliveryStats.droppedHits > 0 { + if packs.deliveryStats.droppedHits > 0 { Label(L("%lld managed-pack decisions were omitted from activity because the extension's bounded queue was full.", - monitor.rulePacks.deliveryStats.droppedHits), systemImage: "exclamationmark.triangle") + packs.deliveryStats.droppedHits), systemImage: "exclamationmark.triangle") .font(.caption).foregroundStyle(FL.warning) .padding(10).background(FL.warning.opacity(0.1), in: RoundedRectangle(cornerRadius: 8)) } - ForEach(monitor.rulePacks.definitions) { pack in + ForEach(packs.definitions) { pack in card(pack) } } .padding(Measure.gutter) } - .sheet(item: $editor) { pack in RulePackEditor(pack: pack) } + .sheet(item: $editor) { request in + RulePackEditor(pack: request.pack, saved: request.saved, intent: request.intent, packs: packs) + } } private var explanation: some View { @@ -33,8 +51,8 @@ struct RulePacksView: View { } private func card(_ pack: RulePackDefinition) -> some View { - let configuration = monitor.rulePacks.configuration(for: pack.id) - let events = monitor.rulePacks.events(for: pack.id) + let configuration = packs.configuration(for: pack.id) + let events = packs.events(for: pack.id) let blocks = events.reduce(0) { $0 + $1.count } return VStack(alignment: .leading, spacing: 10) { HStack(alignment: .top, spacing: 12) { @@ -47,8 +65,13 @@ struct RulePacksView: View { } Spacer(minLength: 8) Toggle("", isOn: Binding(get: { configuration.enabled }, set: { enabled in - if enabled { editor = pack } - else { var changed = configuration; changed.enabled = false; monitor.rulePacks.save(changed) } + if enabled { + editor = RulePackEditorRequest(pack: pack, saved: configuration, intent: .activate) + } else { + var changed = configuration + changed.enabled = false + packs.save(changed) + } })).toggleStyle(.switch).controlSize(.small).labelsHidden() } HStack(spacing: 8) { @@ -56,11 +79,13 @@ struct RulePacksView: View { pill(configuration.scope == .allApps ? L("Every app") : L("%lld selected apps", configuration.apps.count)) pill(L("%lld destinations", pack.entries.count)) if blocks > 0 { pill(L("%lld blocked", blocks), tint: FL.critical) } - if !monitor.rulePacks.overrides(for: pack.id).isEmpty { - pill(L("%lld exceptions", monitor.rulePacks.overrides(for: pack.id).count), tint: FL.good) + if !packs.overrides(for: pack.id).isEmpty { + pill(L("%lld exceptions", packs.overrides(for: pack.id).count), tint: FL.good) } Spacer() - Button(L("Review…")) { editor = pack } + Button(L("Review…")) { + editor = RulePackEditorRequest(pack: pack, saved: configuration, intent: .review) + } } Text(L(pack.manifest.caveatKey)).font(.caption).foregroundStyle(.tertiary) .fixedSize(horizontal: false, vertical: true) @@ -73,13 +98,13 @@ struct RulePacksView: View { private func status(_ configuration: RulePackConfiguration) -> String { if !configuration.enabled { - switch monitor.rulePacks.status { + switch packs.status { case .applying: return L("Applying…") case .failed: return L("Needs review") default: return L("Off") } } - switch monitor.rulePacks.status { + switch packs.status { case .off, .ready, .applying: return monitor.canBlock ? L("Applying…") : L("Watching only") case .enforcing: return monitor.canBlock ? L("Blocking") : L("Watching only") case .watchingOnly: return L("Watching only") @@ -90,12 +115,12 @@ struct RulePacksView: View { private func statusTint(_ configuration: RulePackConfiguration) -> Color? { if !configuration.enabled { - switch monitor.rulePacks.status { + switch packs.status { case .applying, .failed: return FL.warning default: return nil } } - return monitor.canBlock && monitor.rulePacks.status == .enforcing ? FL.critical : FL.warning + return monitor.canBlock && packs.status == .enforcing ? FL.critical : FL.warning } private func symbol(_ pack: RulePackDefinition) -> String { @@ -118,19 +143,19 @@ private struct RulePackEditor: View { let pack: RulePackDefinition @EnvironmentObject var monitor: TrafficMonitor @Environment(\.dismiss) private var dismiss - @State private var configuration: RulePackConfiguration - @State private var originalConfiguration: RulePackConfiguration - @State private var app = "" + @ObservedObject var packs: RulePackStore + @State private var draft: RulePackEditorDraft @State private var installed: [InstalledApps.App] = [] @State private var preview: RuleSimulation.Result? @State private var previewing = false @State private var confirmBroadening = false + @State private var saving = false - init(pack: RulePackDefinition) { + init(pack: RulePackDefinition, saved: RulePackConfiguration, + intent: RulePackEditorDraft.Intent, packs: RulePackStore) { self.pack = pack - let initial = RulePackConfiguration(packID: pack.id) - _configuration = State(initialValue: initial) - _originalConfiguration = State(initialValue: initial) + self.packs = packs + _draft = State(initialValue: RulePackEditorDraft(saved: saved, intent: intent)) } var body: some View { @@ -156,21 +181,16 @@ private struct RulePackEditor: View { Spacer() Button(L("Cancel")) { dismiss() } Button(L("Save")) { save() } - .keyboardShortcut(.defaultAction) - .disabled(configuration.scope == .selectedApps && configuration.apps.isEmpty) + .disabled(!draft.canSave || saving) }.padding(Measure.gutter) } .frame(width: Measure.prose) .frame(minHeight: 620) .onAppear { - let saved = monitor.rulePacks.configuration(for: pack.id) - configuration = saved - if !saved.enabled { configuration.enabled = true } - originalConfiguration = saved Task { installed = await Task.detached(priority: .userInitiated) { InstalledApps.all() }.value } } .confirmationDialog(L("Apply this pack to every app?"), isPresented: $confirmBroadening) { - Button(L("Apply to every app"), role: .destructive) { commit() } + Button(L("Apply to every app"), role: .destructive) { commit(draft.configuration) } Button(L("Cancel"), role: .cancel) {} } message: { Text(L("This broadens blocking from selected apps to every app on this Mac. Essential system traffic remains allowed.")) @@ -180,16 +200,18 @@ private struct RulePackEditor: View { private var scope: some View { VStack(alignment: .leading, spacing: 8) { Text(L("Scope")).font(.headline) - Picker("", selection: $configuration.scope) { + Picker("", selection: $draft.configuration.scope) { Text(L("Every app")).tag(RulePackConfiguration.Scope.allApps) Text(L("Only selected apps")).tag(RulePackConfiguration.Scope.selectedApps) }.pickerStyle(.segmented).labelsHidden() - if configuration.scope == .selectedApps { + if draft.configuration.scope == .selectedApps { HStack { - TextField(L("App name, bundle identifier, or process"), text: $app) - Button(L("Add")) { addApp() }.disabled(app.trimmingCharacters(in: .whitespaces).isEmpty) + TextField(L("App name, bundle identifier, or process"), text: $draft.typedApp) + .onSubmit { addApp() } + Button(L("Add")) { addApp() } + .disabled(draft.typedApp.trimmingCharacters(in: .whitespaces).isEmpty) } - if !app.isEmpty, !appSuggestions.isEmpty { + if !draft.typedApp.isEmpty, !appSuggestions.isEmpty { VStack(alignment: .leading, spacing: 0) { ForEach(appSuggestions) { suggestion in Button { @@ -210,8 +232,8 @@ private struct RulePackEditor: View { } } FlowLayout(spacing: 6) { - ForEach(configuration.apps, id: \.self) { id in - Button { configuration.apps.removeAll { $0 == id } } label: { + ForEach(draft.configuration.apps, id: \.self) { id in + Button { draft.configuration.apps.removeAll { $0 == id } } label: { Label(id, systemImage: "xmark.circle.fill").font(.caption) }.buttonStyle(.borderless) } @@ -258,7 +280,7 @@ private struct RulePackEditor: View { } @ViewBuilder private var exceptions: some View { - let found = monitor.rulePacks.overrides(for: pack.id) + let found = packs.overrides(for: pack.id) if !found.isEmpty { DisclosureGroup(L("%lld exceptions", found.count)) { ForEach(found) { exception in @@ -267,7 +289,7 @@ private struct RulePackEditor: View { Text("→").foregroundStyle(.tertiary) Text(exception.host).font(.caption.monospaced()) Spacer() - Button(L("Delete"), role: .destructive) { monitor.rulePacks.deleteOverride(exception) } + Button(L("Delete"), role: .destructive) { packs.deleteOverride(exception) } }.padding(.vertical, 2) } } @@ -275,32 +297,31 @@ private struct RulePackEditor: View { } private var appSuggestions: [InstalledApps.App] { - Array(InstalledApps.search(app, in: installed).filter { candidate in - !configuration.apps.contains { $0.caseInsensitiveCompare(candidate.bundleID) == .orderedSame } + Array(InstalledApps.search(draft.typedApp, in: installed).filter { candidate in + !draft.configuration.apps.contains { $0.caseInsensitiveCompare(candidate.bundleID) == .orderedSame } }.prefix(5)) } private func addApp(_ value: String? = nil) { - guard let target = InstalledApps.target(forTyped: value ?? app, apps: installed) else { return } - if !configuration.apps.contains(where: { $0.caseInsensitiveCompare(target.value) == .orderedSame }) { - configuration.apps.append(target.value); configuration.apps.sort() - } - app = "" + if let value { _ = draft.addApp(value, installed: installed) } + else { _ = draft.submitApp(installed: installed) } } private func save() { - if originalConfiguration.enabled, - originalConfiguration.scope == .selectedApps, - configuration.scope == .allApps { - confirmBroadening = true - } else { - commit() + switch draft.requestSave() { + case .commit(let configuration): commit(configuration) + case .confirmBroadening: confirmBroadening = true + case nil: break } } - private func commit() { - monitor.rulePacks.save(configuration) - dismiss() + private func commit(_ configuration: RulePackConfiguration? = nil) { + saving = true + let value = configuration ?? draft.configuration + packs.save(value) { success in + saving = false + if success { dismiss() } + } } private func runPreview() async { @@ -309,9 +330,9 @@ private struct RulePackEditor: View { let input = try? await monitor.read { (try $0.flowsForSimulation(since: Date().addingTimeInterval(-7 * 86400)), try $0.loadPolicies()) } - preview = RulePackSimulation.run(candidate: configuration, definitions: monitor.rulePacks.definitions, - configurations: monitor.rulePacks.configurations, - overrides: monitor.rulePacks.overrides, rules: monitor.rules.rules, + preview = RulePackSimulation.run(candidate: draft.configuration, definitions: packs.definitions, + configurations: packs.configurations, + overrides: packs.overrides, rules: monitor.rules.rules, policies: input?.1 ?? [:], paused: monitor.rules.isPaused, flows: input?.0 ?? [], session: RuleStore.session) } diff --git a/FlowlightTests/RulePackTests.swift b/FlowlightTests/RulePackTests.swift index 640a898e..b7e133e5 100644 --- a/FlowlightTests/RulePackTests.swift +++ b/FlowlightTests/RulePackTests.swift @@ -224,6 +224,57 @@ final class RulePackTests: XCTestCase { XCTAssertEqual(publications[0]?.configurations, saved) } + @MainActor + func testStoreDisablesEmptySelectedAppConfiguration() async throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent("flowlight-pack-empty-scope-\(UUID().uuidString).sqlite") + let db = try TrafficDatabase(url: url) + let store = RulePackStore() + store.attach(db: db) + try await waitUntil { store.configurations.count == store.definitions.count } + + var candidate = store.configuration(for: "official.advertising") + candidate.enabled = true + candidate.scope = .selectedApps + candidate.apps = [" ", "\n"] + var completed = false + store.save(candidate) { success in + XCTAssertTrue(success) + completed = true + } + + try await waitUntil { completed } + let committed = store.configuration(for: candidate.packID) + XCTAssertFalse(committed.enabled) + XCTAssertEqual(committed.scope, .selectedApps) + XCTAssertTrue(committed.apps.isEmpty) + let persisted = try db.sync { try $0.loadRulePackConfigurations() } + XCTAssertEqual(persisted, [committed]) + } + + @MainActor + func testStoreSaveCompletionSeesCommittedConfiguration() async throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent("flowlight-pack-completion-\(UUID().uuidString).sqlite") + let db = try TrafficDatabase(url: url) + let store = RulePackStore() + store.attach(db: db) + try await waitUntil { store.configurations.count == store.definitions.count } + + var candidate = store.configuration(for: "official.advertising") + candidate.enabled = true + candidate.scope = .selectedApps + candidate.apps = ["com.apple.curl"] + var completed = false + store.save(candidate) { success in + XCTAssertTrue(success) + XCTAssertEqual(store.configuration(for: candidate.packID).scope, .selectedApps) + XCTAssertEqual(store.configuration(for: candidate.packID).apps, ["com.apple.curl"]) + XCTAssertNotNil(store.snapshot) + completed = true + } + + try await waitUntil { completed } + } + @MainActor func testStorePublishesAfterTransientFailureRecovers() async throws { let url = FileManager.default.temporaryDirectory.appendingPathComponent("flowlight-pack-recovery-\(UUID().uuidString).sqlite") @@ -306,6 +357,59 @@ final class RulePackTests: XCTestCase { XCTAssertNil(store.snapshot) } + func testEditorReviewPreservesOffConfiguration() { + let saved = RulePackConfiguration(packID: "official.advertising") + let draft = RulePackEditorDraft(saved: saved, intent: .review) + + XCTAssertFalse(draft.configuration.enabled) + XCTAssertEqual(draft.configuration.scope, .allApps) + XCTAssertNil(draft.saveAction) + } + + func testEditorActivationAndCancelNeverMutateSavedConfiguration() { + let saved = RulePackConfiguration(packID: "official.advertising") + var draft = RulePackEditorDraft(saved: saved, intent: .activate) + + XCTAssertTrue(draft.configuration.enabled) + draft.configuration.scope = .selectedApps + draft.typedApp = "com.apple.curl" + XCTAssertEqual(draft.submitApp(installed: []), .added("com.apple.curl")) + XCTAssertEqual(draft.configuration.apps, ["com.apple.curl"]) + XCTAssertEqual(saved, RulePackConfiguration(packID: "official.advertising")) + } + + func testEditorReturnAddsAppWithoutSaving() { + let saved = RulePackConfiguration(packID: "official.advertising") + var draft = RulePackEditorDraft(saved: saved, intent: .activate) + draft.configuration.scope = .selectedApps + draft.typedApp = "com.apple.curl" + + XCTAssertEqual(draft.submitApp(installed: []), .added("com.apple.curl")) + XCTAssertEqual(draft.configuration.apps, ["com.apple.curl"]) + XCTAssertEqual(draft.typedApp, "") + XCTAssertNil(draft.saveAction, "Return in the app field must not save or dismiss the editor") + + draft.typedApp = "com.example.second" + XCTAssertEqual(draft.submitApp(installed: []), .added("com.example.second")) + XCTAssertEqual(draft.configuration.apps, ["com.apple.curl", "com.example.second"]) + XCTAssertNil(draft.saveAction, "Return must remain Add even after Save becomes valid") + } + + func testEditorExplicitSaveAndBroadeningDecision() { + var selected = RulePackConfiguration(packID: "official.advertising", enabled: true, + scope: .selectedApps, apps: ["com.apple.curl"]) + var draft = RulePackEditorDraft(saved: selected, intent: .review) + XCTAssertEqual(draft.requestSave(), .commit(selected)) + + draft.configuration.scope = .allApps + XCTAssertEqual(draft.requestSave(), .confirmBroadening) + + selected.apps = [] + var invalid = RulePackEditorDraft(saved: selected, intent: .review) + XCTAssertNil(invalid.saveAction) + XCTAssertFalse(invalid.canSave) + } + func testActivityKeepsPackProvenanceSeparateAndReceiptAfterPrune() throws { let url = FileManager.default.temporaryDirectory.appendingPathComponent("flowlight-pack-provenance-\(UUID().uuidString).sqlite") let db = try TrafficDatabase(url: url) diff --git a/docs/404.html b/docs/404.html index 6f4b3ab2..f58b62be 100644 --- a/docs/404.html +++ b/docs/404.html @@ -86,7 +86,7 @@

    That page isn't here

    Try the home page,

diff --git a/docs/about/index.html b/docs/about/index.html index 5c366412..66e89b79 100644 --- a/docs/about/index.html +++ b/docs/about/index.html @@ -153,7 +153,7 @@

Thanks

diff --git a/docs/de/about/index.html b/docs/de/about/index.html index 6808ea58..2ff7ac90 100644 --- a/docs/de/about/index.html +++ b/docs/de/about/index.html @@ -152,7 +152,7 @@

Dank

diff --git a/docs/de/docs/index.html b/docs/de/docs/index.html index e0428011..da5346ff 100644 --- a/docs/de/docs/index.html +++ b/docs/de/docs/index.html @@ -76,7 +76,7 @@

Dokumentation

Flowlight benutzen

-

Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.15.0, macOS 15 oder neuer.

+

Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.15.1, macOS 15 oder neuer.

diff --git a/docs/de/index.html b/docs/de/index.html index 7936bf72..533c8140 100644 --- a/docs/de/index.html +++ b/docs/de/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

Sieh, was deine Apps im Netz tun.
brew install --cask xinbetween/tap/flowlight

-

v0.15.0macOS 15+UniversalGPL-3.0Keine Telemetrie

+

v0.15.1macOS 15+UniversalGPL-3.0Keine Telemetrie

@@ -571,7 +571,7 @@

Wisse, was deinen Mac verlässt.

diff --git a/docs/de/privacy/index.html b/docs/de/privacy/index.html index 9eb30c0d..b0d8e609 100644 --- a/docs/de/privacy/index.html +++ b/docs/de/privacy/index.html @@ -188,7 +188,7 @@

Kontakt

diff --git a/docs/de/threat-model/index.html b/docs/de/threat-model/index.html index a0f6e895..3bcc4a83 100644 --- a/docs/de/threat-model/index.html +++ b/docs/de/threat-model/index.html @@ -228,7 +228,7 @@

Eine Schwachstelle melden

diff --git a/docs/docs/index.html b/docs/docs/index.html index b6394a41..f841d88a 100644 --- a/docs/docs/index.html +++ b/docs/docs/index.html @@ -76,7 +76,7 @@

Documentation

Using Flowlight

-

Everything from the first launch to tuning the agent rules. Flowlight 0.15.0, macOS 15 or later.

+

Everything from the first launch to tuning the agent rules. Flowlight 0.15.1, macOS 15 or later.

diff --git a/docs/es/about/index.html b/docs/es/about/index.html index 97f565d1..c860c5eb 100644 --- a/docs/es/about/index.html +++ b/docs/es/about/index.html @@ -152,7 +152,7 @@

Agradecimientos

diff --git a/docs/es/docs/index.html b/docs/es/docs/index.html index 29d750cf..b93b3f5b 100644 --- a/docs/es/docs/index.html +++ b/docs/es/docs/index.html @@ -76,7 +76,7 @@

Documentación

Usar Flowlight

-

Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.15.0, macOS 15 o posterior.

+

Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.15.1, macOS 15 o posterior.

diff --git a/docs/es/index.html b/docs/es/index.html index d49e808b..74ba3f12 100644 --- a/docs/es/index.html +++ b/docs/es/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

Observa la actividad de red de tus apps.

brew install --cask xinbetween/tap/flowlight

-

v0.15.0macOS 15+UniversalGPL-3.0Sin telemetría

+

v0.15.1macOS 15+UniversalGPL-3.0Sin telemetría

@@ -571,7 +571,7 @@

Ten claro qué sale de tu Mac.

diff --git a/docs/es/privacy/index.html b/docs/es/privacy/index.html index 228cb48d..dd16995b 100644 --- a/docs/es/privacy/index.html +++ b/docs/es/privacy/index.html @@ -188,7 +188,7 @@

Contacto

diff --git a/docs/es/threat-model/index.html b/docs/es/threat-model/index.html index 6cef695b..4f8bbb36 100644 --- a/docs/es/threat-model/index.html +++ b/docs/es/threat-model/index.html @@ -223,7 +223,7 @@

Informar de una vulnerabilidad

diff --git a/docs/fr/about/index.html b/docs/fr/about/index.html index bac0cf2a..bc3091bf 100644 --- a/docs/fr/about/index.html +++ b/docs/fr/about/index.html @@ -152,7 +152,7 @@

Remerciements

diff --git a/docs/fr/docs/index.html b/docs/fr/docs/index.html index 25e3f2d5..1e615d9f 100644 --- a/docs/fr/docs/index.html +++ b/docs/fr/docs/index.html @@ -76,7 +76,7 @@

Documentation

Utiliser Flowlight

-

Tout, du premier lancement au réglage des règles des agents. Flowlight 0.15.0, macOS 15 ou version ultérieure.

+

Tout, du premier lancement au réglage des règles des agents. Flowlight 0.15.1, macOS 15 ou version ultérieure.

diff --git a/docs/fr/index.html b/docs/fr/index.html index cf0c4e6d..2b1cba85 100644 --- a/docs/fr/index.html +++ b/docs/fr/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

Voyez l’activité réseau de vos apps.

brew install --cask xinbetween/tap/flowlight

-

v0.15.0macOS 15+UniverselGPL-3.0Sans télémétrie

+

v0.15.1macOS 15+UniverselGPL-3.0Sans télémétrie

@@ -571,7 +571,7 @@

Sachez ce qui quitte votre Mac.

diff --git a/docs/fr/privacy/index.html b/docs/fr/privacy/index.html index 644ea7a6..f7248635 100644 --- a/docs/fr/privacy/index.html +++ b/docs/fr/privacy/index.html @@ -188,7 +188,7 @@

Contact

diff --git a/docs/fr/threat-model/index.html b/docs/fr/threat-model/index.html index bf75af78..376378f9 100644 --- a/docs/fr/threat-model/index.html +++ b/docs/fr/threat-model/index.html @@ -226,7 +226,7 @@

Signaler une vulnérabilité

diff --git a/docs/index.html b/docs/index.html index 7f5f7e26..a66a9a77 100644 --- a/docs/index.html +++ b/docs/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

See your apps' network activity.
brew install --cask xinbetween/tap/flowlight

-

v0.15.0macOS 15+UniversalGPL-3.0No telemetry

+

v0.15.1macOS 15+UniversalGPL-3.0No telemetry

@@ -592,7 +592,7 @@

Know what leaves your Mac.

diff --git a/docs/it/about/index.html b/docs/it/about/index.html index 4e8d6d9d..c3ef1346 100644 --- a/docs/it/about/index.html +++ b/docs/it/about/index.html @@ -152,7 +152,7 @@

Ringraziamenti

diff --git a/docs/it/docs/index.html b/docs/it/docs/index.html index f0aa3c17..46707524 100644 --- a/docs/it/docs/index.html +++ b/docs/it/docs/index.html @@ -76,7 +76,7 @@

Documentazione

Usare Flowlight

-

Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.15.0, macOS 15 o successivo.

+

Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.15.1, macOS 15 o successivo.

diff --git a/docs/it/index.html b/docs/it/index.html index 1384eee2..e04dd8e7 100644 --- a/docs/it/index.html +++ b/docs/it/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

Vedi la rete delle tue app.
Ca Metti una stella su GitHub

brew install --cask xinbetween/tap/flowlight

-

v0.15.0macOS 15+UniversaleGPL-3.0Nessuna telemetria

+

v0.15.1macOS 15+UniversaleGPL-3.0Nessuna telemetria

@@ -571,7 +571,7 @@

Sappi che cosa esce dal tuo Mac.

diff --git a/docs/it/privacy/index.html b/docs/it/privacy/index.html index 2766b308..0e41922a 100644 --- a/docs/it/privacy/index.html +++ b/docs/it/privacy/index.html @@ -188,7 +188,7 @@

Contatti

diff --git a/docs/it/threat-model/index.html b/docs/it/threat-model/index.html index 73492b36..cd636f1a 100644 --- a/docs/it/threat-model/index.html +++ b/docs/it/threat-model/index.html @@ -224,7 +224,7 @@

Segnalare una vulnerabilità

diff --git a/docs/ja/about/index.html b/docs/ja/about/index.html index a4157790..254e1a95 100644 --- a/docs/ja/about/index.html +++ b/docs/ja/about/index.html @@ -153,7 +153,7 @@

謝辞

diff --git a/docs/ja/docs/index.html b/docs/ja/docs/index.html index 46d08657..9e64ff3d 100644 --- a/docs/ja/docs/index.html +++ b/docs/ja/docs/index.html @@ -76,7 +76,7 @@

ドキュメント

Flowlight の使い方

-

初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.15.0、macOS 15 以降。

+

初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.15.1、macOS 15 以降。

diff --git a/docs/ja/index.html b/docs/ja/index.html index 4d51fe7b..f804e52d 100644 --- a/docs/ja/index.html +++ b/docs/ja/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

アプリの通信が見える。
brew install --cask xinbetween/tap/flowlight

-

v0.15.0macOS 15+ユニバーサルGPL-3.0テレメトリなし

+

v0.15.1macOS 15+ユニバーサルGPL-3.0テレメトリなし

@@ -570,7 +570,7 @@

Mac から何が出ていくのかを知る。

diff --git a/docs/ja/privacy/index.html b/docs/ja/privacy/index.html index c50ff695..ca6c1cd0 100644 --- a/docs/ja/privacy/index.html +++ b/docs/ja/privacy/index.html @@ -186,7 +186,7 @@

連絡先

diff --git a/docs/ja/threat-model/index.html b/docs/ja/threat-model/index.html index 43a0fe32..49c128a7 100644 --- a/docs/ja/threat-model/index.html +++ b/docs/ja/threat-model/index.html @@ -225,7 +225,7 @@

脆弱性を報告する

diff --git a/docs/ko/about/index.html b/docs/ko/about/index.html index b7c8bb1b..bdb31f5e 100644 --- a/docs/ko/about/index.html +++ b/docs/ko/about/index.html @@ -153,7 +153,7 @@

감사

diff --git a/docs/ko/docs/index.html b/docs/ko/docs/index.html index a0a04598..8abc02f2 100644 --- a/docs/ko/docs/index.html +++ b/docs/ko/docs/index.html @@ -76,7 +76,7 @@

문서

Flowlight 사용하기

-

첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.15.0, macOS 15 이상.

+

첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.15.1, macOS 15 이상.

diff --git a/docs/ko/index.html b/docs/ko/index.html index 2194f32d..03b89a4d 100644 --- a/docs/ko/index.html +++ b/docs/ko/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

앱이 무엇을 하는지 봅니다.

brew install --cask xinbetween/tap/flowlight

-

v0.15.0macOS 15+유니버설GPL-3.0텔레메트리 없음

+

v0.15.1macOS 15+유니버설GPL-3.0텔레메트리 없음

@@ -571,7 +571,7 @@

내 Mac에서 무엇이 나가는지 아세요.

diff --git a/docs/ko/privacy/index.html b/docs/ko/privacy/index.html index 92c37acb..09d889c6 100644 --- a/docs/ko/privacy/index.html +++ b/docs/ko/privacy/index.html @@ -189,7 +189,7 @@

문의

diff --git a/docs/ko/threat-model/index.html b/docs/ko/threat-model/index.html index f2458717..a4e8f2d9 100644 --- a/docs/ko/threat-model/index.html +++ b/docs/ko/threat-model/index.html @@ -223,7 +223,7 @@

취약점 신고하기

diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 56e119ba..5cc393a3 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -62,7 +62,7 @@ Documentation Using Flowlight - Everything from the first launch to tuning the agent rules. Flowlight 0.15.0, macOS 15 or later. + Everything from the first launch to tuning the agent rules. Flowlight 0.15.1, macOS 15 or later. On this page @@ -887,7 +887,7 @@ Understand your AI agents. brew install --cask xinbetween/tap/flowlightCopy - v0.15.0macOS 15+UniversalGPL-3.0No telemetry + v0.15.1macOS 15+UniversalGPL-3.0No telemetry connectionslive @@ -1617,8 +1617,20 @@ Releases Downloads, checksums and full notes for each version are on GitHub Releases. + 0.15.1 +October 18, 2026Latest + + Keep managed-pack changes safely in review. Opening or cancelling an off pack no longer + activates it, and configuration changes persist only after an explicit Save succeeds. + + Add selected apps without closing the editor. Pressing Return in the app field now adds + the typed app or process to the draft instead of triggering the sheet's Save action. + + Show the configuration that actually committed. Pack cards now observe managed-pack state + directly, so scope, status, exceptions and activity refresh as soon as an asynchronous save completes. + 0.15.0 -October 17, 2026Latest +October 17, 2026 Block known ad and tracker destinations across apps. Three official managed packs cover advertising, cross-site tracking, and privacy/telemetry hosts. They ship disabled, require explicit review, diff --git a/docs/llms.txt b/docs/llms.txt index bc4480cd..b6693510 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -1,6 +1,6 @@ # Flowlight -> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.15.0. +> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.15.1. - [Download Flowlight.dmg](https://github.com/xinbetween/flowlight/releases/latest/download/Flowlight.dmg) - [Source code](https://github.com/xinbetween/flowlight) diff --git a/docs/plugins/index.html b/docs/plugins/index.html index 3267f5d1..0ba7b180 100644 --- a/docs/plugins/index.html +++ b/docs/plugins/index.html @@ -211,7 +211,7 @@

Review checklist

diff --git a/docs/privacy/index.html b/docs/privacy/index.html index 1c45a745..a9063d7a 100644 --- a/docs/privacy/index.html +++ b/docs/privacy/index.html @@ -195,7 +195,7 @@

Contact

diff --git a/docs/pt-PT/about/index.html b/docs/pt-PT/about/index.html index be10595b..01d336ad 100644 --- a/docs/pt-PT/about/index.html +++ b/docs/pt-PT/about/index.html @@ -152,7 +152,7 @@

Agradecimentos

diff --git a/docs/pt-PT/docs/index.html b/docs/pt-PT/docs/index.html index 20bfd264..8696733e 100644 --- a/docs/pt-PT/docs/index.html +++ b/docs/pt-PT/docs/index.html @@ -76,7 +76,7 @@

Documentação

Usar o Flowlight

-

Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.15.0, macOS 15 ou posterior.

+

Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.15.1, macOS 15 ou posterior.

diff --git a/docs/pt-PT/index.html b/docs/pt-PT/index.html index 6169e0aa..70178b72 100644 --- a/docs/pt-PT/index.html +++ b/docs/pt-PT/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

Veja a atividade de rede das suas apps.

brew install --cask xinbetween/tap/flowlight

-

v0.15.0macOS 15+UniversalGPL-3.0Sem telemetria

+

v0.15.1macOS 15+UniversalGPL-3.0Sem telemetria

@@ -571,7 +571,7 @@

Saiba o que sai do seu Mac.

diff --git a/docs/pt-PT/privacy/index.html b/docs/pt-PT/privacy/index.html index d042d6ff..c8f83100 100644 --- a/docs/pt-PT/privacy/index.html +++ b/docs/pt-PT/privacy/index.html @@ -188,7 +188,7 @@

Contacto

diff --git a/docs/pt-PT/threat-model/index.html b/docs/pt-PT/threat-model/index.html index 188600cc..e98cb423 100644 --- a/docs/pt-PT/threat-model/index.html +++ b/docs/pt-PT/threat-model/index.html @@ -225,7 +225,7 @@

Comunicar uma vulnerabilidade

diff --git a/docs/releases/index.html b/docs/releases/index.html index c721081e..f81c084d 100644 --- a/docs/releases/index.html +++ b/docs/releases/index.html @@ -28,7 +28,7 @@ - + @@ -61,7 +61,19 @@

What's new

-

0.15.0

Latest
+

0.15.1

Latest
+
    +
  • Keep managed-pack changes safely in review. Opening or cancelling an off pack no longer + activates it, and configuration changes persist only after an explicit Save succeeds.
  • +
  • Add selected apps without closing the editor. Pressing Return in the app field now adds + the typed app or process to the draft instead of triggering the sheet's Save action.
  • +
  • Show the configuration that actually committed. Pack cards now observe managed-pack state + directly, so scope, status, exceptions and activity refresh as soon as an asynchronous save completes.
  • +
+
+ +
+

0.15.0

  • Block known ad and tracker destinations across apps. Three official managed packs cover advertising, cross-site tracking, and privacy/telemetry hosts. They ship disabled, require explicit review, @@ -1233,7 +1245,7 @@

    What's new

diff --git a/docs/sitemap.xml b/docs/sitemap.xml index 5b51fb07..9253a5e4 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -5,7 +5,7 @@ https://flowlight.xinbetween.com/2026-10-17 https://flowlight.xinbetween.com/plugins/2026-10-13 https://flowlight.xinbetween.com/privacy/2026-10-07 - https://flowlight.xinbetween.com/releases/2026-10-17 + https://flowlight.xinbetween.com/releases/2026-10-18 https://flowlight.xinbetween.com/threat-model/2026-09-27 https://flowlight.xinbetween.com/de/about/2026-09-26 https://flowlight.xinbetween.com/de/docs/2026-09-28 diff --git a/docs/threat-model/index.html b/docs/threat-model/index.html index f9395ed0..1d879978 100644 --- a/docs/threat-model/index.html +++ b/docs/threat-model/index.html @@ -224,7 +224,7 @@

Reporting a vulnerability

diff --git a/docs/zh-Hans/about/index.html b/docs/zh-Hans/about/index.html index 83befd0a..60f968de 100644 --- a/docs/zh-Hans/about/index.html +++ b/docs/zh-Hans/about/index.html @@ -152,7 +152,7 @@

致谢

diff --git a/docs/zh-Hans/docs/index.html b/docs/zh-Hans/docs/index.html index b45fce5c..684152b1 100644 --- a/docs/zh-Hans/docs/index.html +++ b/docs/zh-Hans/docs/index.html @@ -76,7 +76,7 @@

文档

使用 Flowlight

-

从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.15.0,macOS 15 或更高版本。

+

从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.15.1,macOS 15 或更高版本。

diff --git a/docs/zh-Hans/index.html b/docs/zh-Hans/index.html index 06504594..fd06a1b1 100644 --- a/docs/zh-Hans/index.html +++ b/docs/zh-Hans/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

看清应用的网络活动。

brew install --cask xinbetween/tap/flowlight

-

v0.15.0macOS 15+通用架构GPL-3.0无遥测

+

v0.15.1macOS 15+通用架构GPL-3.0无遥测

@@ -568,7 +568,7 @@

知道什么离开了你的 Mac。

diff --git a/docs/zh-Hans/privacy/index.html b/docs/zh-Hans/privacy/index.html index aee7d31e..090e8ea1 100644 --- a/docs/zh-Hans/privacy/index.html +++ b/docs/zh-Hans/privacy/index.html @@ -186,7 +186,7 @@

联系方式

diff --git a/docs/zh-Hans/threat-model/index.html b/docs/zh-Hans/threat-model/index.html index 1d0afd87..679dd69f 100644 --- a/docs/zh-Hans/threat-model/index.html +++ b/docs/zh-Hans/threat-model/index.html @@ -212,7 +212,7 @@

报告漏洞

diff --git a/docs/zh-Hant/about/index.html b/docs/zh-Hant/about/index.html index cb3ba5d0..da99e59f 100644 --- a/docs/zh-Hant/about/index.html +++ b/docs/zh-Hant/about/index.html @@ -152,7 +152,7 @@

致謝

diff --git a/docs/zh-Hant/docs/index.html b/docs/zh-Hant/docs/index.html index d34700f1..b207822a 100644 --- a/docs/zh-Hant/docs/index.html +++ b/docs/zh-Hant/docs/index.html @@ -76,7 +76,7 @@

說明文件

使用 Flowlight

-

從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.15.0,macOS 15 或以上版本。

+

從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.15.1,macOS 15 或以上版本。

diff --git a/docs/zh-Hant/index.html b/docs/zh-Hant/index.html index 047c262c..dee17a05 100644 --- a/docs/zh-Hant/index.html +++ b/docs/zh-Hant/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

看見每個 App 的網路活動。
brew install --cask xinbetween/tap/flowlight

-

v0.15.0macOS 15+通用架構GPL-3.0無遙測

+

v0.15.1macOS 15+通用架構GPL-3.0無遙測

@@ -569,7 +569,7 @@

知道有什麼離開了你的 Mac。

diff --git a/docs/zh-Hant/privacy/index.html b/docs/zh-Hant/privacy/index.html index fa1253a2..4d878aab 100644 --- a/docs/zh-Hant/privacy/index.html +++ b/docs/zh-Hant/privacy/index.html @@ -186,7 +186,7 @@

聯絡

diff --git a/docs/zh-Hant/threat-model/index.html b/docs/zh-Hant/threat-model/index.html index 97fa0803..7865721a 100644 --- a/docs/zh-Hant/threat-model/index.html +++ b/docs/zh-Hant/threat-model/index.html @@ -213,7 +213,7 @@

回報漏洞

diff --git a/project.yml b/project.yml index 8325e17f..0c724f03 100644 --- a/project.yml +++ b/project.yml @@ -11,8 +11,8 @@ settings: DEVELOPMENT_TEAM: "" CODE_SIGN_STYLE: Automatic ENABLE_HARDENED_RUNTIME: YES - MARKETING_VERSION: "0.15.0" - CURRENT_PROJECT_VERSION: "36" + MARKETING_VERSION: "0.15.1" + CURRENT_PROJECT_VERSION: "37" targets: Flowlight: type: application diff --git a/site/pages/releases.html b/site/pages/releases.html index 56d6bdd8..6cc47e07 100644 --- a/site/pages/releases.html +++ b/site/pages/releases.html @@ -13,7 +13,19 @@

What's new

-

0.15.0

Latest
+

0.15.1

Latest
+
    +
  • Keep managed-pack changes safely in review. Opening or cancelling an off pack no longer + activates it, and configuration changes persist only after an explicit Save succeeds.
  • +
  • Add selected apps without closing the editor. Pressing Return in the app field now adds + the typed app or process to the draft instead of triggering the sheet's Save action.
  • +
  • Show the configuration that actually committed. Pack cards now observe managed-pack state + directly, so scope, status, exceptions and activity refresh as soon as an asynchronous save completes.
  • +
+
+ +
+

0.15.0

  • Block known ad and tracker destinations across apps. Three official managed packs cover advertising, cross-site tracking, and privacy/telemetry hosts. They ship disabled, require explicit review, From 876fe93e0557f2630d1ebf0d16cae1a72a7b7d0c Mon Sep 17 00:00:00 2001 From: blessdyb Date: Sun, 18 Oct 2026 08:28:28 -0700 Subject: [PATCH 3/5] Harden managed pack editor saves Co-Authored-By: Claude Code --- Flowlight/Analysis/InstalledApps.swift | 29 ++++-- Flowlight/Analysis/RulePackStore.swift | 12 ++- Flowlight/UI/RulePackEditorDraft.swift | 21 ++-- Flowlight/UI/RulePacksView.swift | 138 ++++++++++++++++++++----- FlowlightTests/RulePackTests.swift | 38 ++++++- 5 files changed, 187 insertions(+), 51 deletions(-) diff --git a/Flowlight/Analysis/InstalledApps.swift b/Flowlight/Analysis/InstalledApps.swift index c4ed9c92..4fc96db3 100644 --- a/Flowlight/Analysis/InstalledApps.swift +++ b/Flowlight/Analysis/InstalledApps.swift @@ -17,8 +17,13 @@ enum InstalledApps { var bluetoothPurpose: String? } + struct ScanResult: Sendable { + var apps: [App] + var complete: Bool + } + private static let lock = NSLock() - private static var cached: [App]? + private static var cached: ScanResult? /// Where applications live. The system folder is included because Safari and Mail are perfectly reasonable /// things to focus on, and they aren't in /Applications. @@ -29,15 +34,22 @@ enum InstalledApps { /// Scanned once and kept. A Mac doesn't gain applications while a popover is open, and re-reading several /// hundred Info.plists on every keystroke would be felt. - static func all() -> [App] { + static func all() -> [App] { scan().apps } + + static func scan() -> ScanResult { lock.lock() if let cached { lock.unlock(); return cached } lock.unlock() var found: [String: App] = [:] + var complete = true let manager = FileManager.default for directory in searchPaths { - guard let entries = try? manager.contentsOfDirectory(atPath: directory) else { continue } + guard manager.fileExists(atPath: directory) else { continue } + guard let entries = try? manager.contentsOfDirectory(atPath: directory) else { + complete = false + continue + } for entry in entries where entry.hasSuffix(".app") { let path = directory + "/" + entry guard let bundle = Bundle(path: path), let id = bundle.bundleIdentifier, !id.isEmpty else { continue } @@ -49,9 +61,14 @@ enum InstalledApps { found[id] = App(name: name, bundleID: id, path: path, bluetoothPurpose: purpose) } } - let sorted = found.values.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } - lock.lock(); cached = sorted; lock.unlock() - return sorted + let result = ScanResult( + apps: found.values.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }, + complete: complete && !found.isEmpty + ) + if result.complete { + lock.lock(); cached = result; lock.unlock() + } + return result } /// The applications built to use Bluetooth, as they say so themselves in their Info.plist. diff --git a/Flowlight/Analysis/RulePackStore.swift b/Flowlight/Analysis/RulePackStore.swift index 718a29c7..acede341 100644 --- a/Flowlight/Analysis/RulePackStore.swift +++ b/Flowlight/Analysis/RulePackStore.swift @@ -31,6 +31,10 @@ final class RulePackStore: ObservableObject { var enabledCount: Int { configurations.filter(\.enabled).count } var hasEnabledPacks: Bool { enabledCount > 0 } + var failureMessage: String? { + if case .failed(let message) = status { return message } + return nil + } func configuration(for packID: String) -> RulePackConfiguration { configurations.first { $0.packID == packID } ?? RulePackConfiguration(packID: packID) @@ -185,7 +189,13 @@ final class RulePackStore: ObservableObject { return } db.queue.async { [weak self, weak db] in - guard let db else { return } + guard let db else { + Task { @MainActor in + self?.status = .failed("Rule pack storage is unavailable.") + onFailure?() + } + return + } do { try write(db) Task { @MainActor in commit() } diff --git a/Flowlight/UI/RulePackEditorDraft.swift b/Flowlight/UI/RulePackEditorDraft.swift index 279ccca3..92873225 100644 --- a/Flowlight/UI/RulePackEditorDraft.swift +++ b/Flowlight/UI/RulePackEditorDraft.swift @@ -9,6 +9,7 @@ struct RulePackEditorDraft { } enum AppSubmission: Equatable { + case loading case ignored case added(String) } @@ -21,7 +22,6 @@ struct RulePackEditorDraft { let originalConfiguration: RulePackConfiguration var configuration: RulePackConfiguration var typedApp = "" - private(set) var saveAction: SaveAction? init(saved: RulePackConfiguration, intent: Intent) { originalConfiguration = saved @@ -33,11 +33,12 @@ struct RulePackEditorDraft { configuration.scope != .selectedApps || !configuration.apps.isEmpty } - mutating func submitApp(installed: [InstalledApps.App]) -> AppSubmission { - addApp(typedApp, installed: installed) + mutating func submitApp(installed: [InstalledApps.App], appsLoaded: Bool = true) -> AppSubmission { + addApp(typedApp, installed: installed, appsLoaded: appsLoaded) } - mutating func addApp(_ value: String, installed: [InstalledApps.App]) -> AppSubmission { + mutating func addApp(_ value: String, installed: [InstalledApps.App], appsLoaded: Bool = true) -> AppSubmission { + guard appsLoaded else { return .loading } guard let target = InstalledApps.target(forTyped: value, apps: installed) else { return .ignored } if !configuration.apps.contains(where: { $0.caseInsensitiveCompare(target.value) == .orderedSame }) { configuration.apps.append(target.value) @@ -47,17 +48,13 @@ struct RulePackEditorDraft { return .added(target.value) } - mutating func requestSave() -> SaveAction? { - guard canSave else { saveAction = nil; return nil } + func requestSave() -> SaveAction? { + guard canSave else { return nil } if originalConfiguration.enabled, originalConfiguration.scope == .selectedApps, configuration.scope == .allApps { - saveAction = .confirmBroadening - } else { - saveAction = .commit(configuration) + return .confirmBroadening } - return saveAction + return .commit(configuration) } - - mutating func clearSaveAction() { saveAction = nil } } diff --git a/Flowlight/UI/RulePacksView.swift b/Flowlight/UI/RulePacksView.swift index 8a9d60ee..4d4a0cd9 100644 --- a/Flowlight/UI/RulePacksView.swift +++ b/Flowlight/UI/RulePacksView.swift @@ -10,15 +10,24 @@ struct RulePacksView: View { private struct RulePackEditorRequest: Identifiable { let pack: RulePackDefinition - let saved: RulePackConfiguration let intent: RulePackEditorDraft.Intent var id: String { pack.id } } +struct RulePackSaveTracker { + private(set) var packIDs: Set = [] + + func contains(_ packID: String) -> Bool { packIDs.contains(packID) } + mutating func begin(_ packID: String) { packIDs.insert(packID) } + mutating func finish(_ packID: String) { packIDs.remove(packID) } +} + private struct RulePacksContent: View { @EnvironmentObject var monitor: TrafficMonitor @ObservedObject var packs: RulePackStore @State private var editor: RulePackEditorRequest? + @State private var saves = RulePackSaveTracker() + @State private var saveErrors: [String: String] = [:] var body: some View { ScrollView { @@ -37,8 +46,10 @@ private struct RulePacksContent: View { .padding(Measure.gutter) } .sheet(item: $editor) { request in - RulePackEditor(pack: request.pack, saved: request.saved, intent: request.intent, packs: packs) + RulePackEditor(pack: request.pack, saved: packs.configuration(for: request.pack.id), + intent: request.intent, packs: packs, rules: monitor.rules) } + .onChange(of: packs.version) { _, _ in saveErrors.removeAll() } } private var explanation: some View { @@ -54,6 +65,8 @@ private struct RulePacksContent: View { let configuration = packs.configuration(for: pack.id) let events = packs.events(for: pack.id) let blocks = events.reduce(0) { $0 + $1.count } + let exceptions = packs.overrides(for: pack.id) + let isSaving = saves.contains(pack.id) return VStack(alignment: .leading, spacing: 10) { HStack(alignment: .top, spacing: 12) { Image(systemName: symbol(pack)).font(.title3).foregroundStyle(configuration.enabled ? FL.critical : .secondary) @@ -66,26 +79,35 @@ private struct RulePacksContent: View { Spacer(minLength: 8) Toggle("", isOn: Binding(get: { configuration.enabled }, set: { enabled in if enabled { - editor = RulePackEditorRequest(pack: pack, saved: configuration, intent: .activate) + editor = RulePackEditorRequest(pack: pack, intent: .activate) } else { var changed = configuration changed.enabled = false - packs.save(changed) + saves.begin(pack.id) + saveErrors[pack.id] = nil + packs.save(changed) { success in + saves.finish(pack.id) + if !success { saveErrors[pack.id] = packs.failureMessage ?? L("Failed: %@", L("Save")) } + } } - })).toggleStyle(.switch).controlSize(.small).labelsHidden() + })).toggleStyle(.switch).controlSize(.small).labelsHidden().disabled(isSaving) } HStack(spacing: 8) { pill(status(configuration), tint: statusTint(configuration)) pill(configuration.scope == .allApps ? L("Every app") : L("%lld selected apps", configuration.apps.count)) pill(L("%lld destinations", pack.entries.count)) if blocks > 0 { pill(L("%lld blocked", blocks), tint: FL.critical) } - if !packs.overrides(for: pack.id).isEmpty { - pill(L("%lld exceptions", packs.overrides(for: pack.id).count), tint: FL.good) + if !exceptions.isEmpty { + pill(L("%lld exceptions", exceptions.count), tint: FL.good) } Spacer() Button(L("Review…")) { - editor = RulePackEditorRequest(pack: pack, saved: configuration, intent: .review) - } + editor = RulePackEditorRequest(pack: pack, intent: .review) + }.disabled(isSaving) + } + if let saveError = saveErrors[pack.id] { + Label(saveError, systemImage: "exclamationmark.triangle") + .font(.caption).foregroundStyle(FL.critical) } Text(L(pack.manifest.caveatKey)).font(.caption).foregroundStyle(.tertiary) .fixedSize(horizontal: false, vertical: true) @@ -144,17 +166,23 @@ private struct RulePackEditor: View { @EnvironmentObject var monitor: TrafficMonitor @Environment(\.dismiss) private var dismiss @ObservedObject var packs: RulePackStore + @ObservedObject var rules: RuleStore @State private var draft: RulePackEditorDraft @State private var installed: [InstalledApps.App] = [] + @State private var appsLoaded = false + @State private var appsScanned = false @State private var preview: RuleSimulation.Result? @State private var previewing = false + @State private var previewError: String? + @State private var saveError: String? @State private var confirmBroadening = false @State private var saving = false init(pack: RulePackDefinition, saved: RulePackConfiguration, - intent: RulePackEditorDraft.Intent, packs: RulePackStore) { + intent: RulePackEditorDraft.Intent, packs: RulePackStore, rules: RuleStore) { self.pack = pack self.packs = packs + self.rules = rules _draft = State(initialValue: RulePackEditorDraft(saved: saved, intent: intent)) } @@ -174,21 +202,30 @@ private struct RulePackEditor: View { exceptions }.padding(Measure.gutter) } + .disabled(saving) + if let saveError { + Label(saveError, systemImage: "exclamationmark.triangle") + .font(.caption).foregroundStyle(FL.critical) + .padding(.horizontal, Measure.gutter).padding(.bottom, 8) + } Divider() HStack { Button(L("Test against recent traffic")) { Task { await runPreview() } } + .disabled(previewing || saving) if previewing { ProgressView().controlSize(.small) } Spacer() - Button(L("Cancel")) { dismiss() } + Button(L("Cancel")) { dismiss() }.disabled(saving) Button(L("Save")) { save() } .disabled(!draft.canSave || saving) }.padding(Measure.gutter) } .frame(width: Measure.prose) .frame(minHeight: 620) - .onAppear { - Task { installed = await Task.detached(priority: .userInitiated) { InstalledApps.all() }.value } - } + .interactiveDismissDisabled(saving) + .onChange(of: draft.configuration) { _, _ in invalidatePreview() } + .onChange(of: packs.version) { _, _ in invalidatePreview() } + .onChange(of: rules.version) { _, _ in invalidatePreview() } + .onAppear { loadApps() } .confirmationDialog(L("Apply this pack to every app?"), isPresented: $confirmBroadening) { Button(L("Apply to every app"), role: .destructive) { commit(draft.configuration) } Button(L("Cancel"), role: .cancel) {} @@ -207,9 +244,14 @@ private struct RulePackEditor: View { if draft.configuration.scope == .selectedApps { HStack { TextField(L("App name, bundle identifier, or process"), text: $draft.typedApp) - .onSubmit { addApp() } + .onSubmit { if appsLoaded { addApp() } } + if !appsScanned { + ProgressView().controlSize(.small) + } else if !appsLoaded { + Button(L("Refresh")) { loadApps() }.controlSize(.small) + } Button(L("Add")) { addApp() } - .disabled(draft.typedApp.trimmingCharacters(in: .whitespaces).isEmpty) + .disabled(!appsLoaded || draft.typedApp.trimmingCharacters(in: .whitespaces).isEmpty) } if !draft.typedApp.isEmpty, !appSuggestions.isEmpty { VStack(alignment: .leading, spacing: 0) { @@ -249,7 +291,11 @@ private struct RulePackEditor: View { } @ViewBuilder private var previewSection: some View { - if let preview { + if let previewError { + Label(previewError, systemImage: "exclamationmark.triangle") + .font(.caption).foregroundStyle(FL.critical) + .padding(10).background(FL.critical.opacity(0.1), in: RoundedRectangle(cornerRadius: 8)) + } else if let preview { VStack(alignment: .leading, spacing: 5) { Text(L("Recent-traffic impact")).font(.headline) if preview.connectionsConsidered == 0 { @@ -302,12 +348,24 @@ private struct RulePackEditor: View { }.prefix(5)) } + private func loadApps() { + appsScanned = false + appsLoaded = false + Task { + let result = await Task.detached(priority: .userInitiated) { InstalledApps.scan() }.value + installed = result.apps + appsLoaded = result.complete + appsScanned = true + } + } + private func addApp(_ value: String? = nil) { - if let value { _ = draft.addApp(value, installed: installed) } - else { _ = draft.submitApp(installed: installed) } + if let value { _ = draft.addApp(value, installed: installed, appsLoaded: appsLoaded) } + else { _ = draft.submitApp(installed: installed, appsLoaded: appsLoaded) } } private func save() { + saveError = nil switch draft.requestSave() { case .commit(let configuration): commit(configuration) case .confirmBroadening: confirmBroadening = true @@ -317,24 +375,50 @@ private struct RulePackEditor: View { private func commit(_ configuration: RulePackConfiguration? = nil) { saving = true + saveError = nil let value = configuration ?? draft.configuration packs.save(value) { success in saving = false - if success { dismiss() } + if success { + dismiss() + } else { + saveError = packs.failureMessage ?? L("Failed: %@", L("Save")) + } } } + private func invalidatePreview() { + preview = nil + previewError = nil + } + private func runPreview() async { + let candidate = draft.configuration + let packVersion = packs.version + let ruleVersion = rules.version previewing = true + preview = nil + previewError = nil defer { previewing = false } - let input = try? await monitor.read { - (try $0.flowsForSimulation(since: Date().addingTimeInterval(-7 * 86400)), try $0.loadPolicies()) + do { + let input = try await monitor.read { + (try $0.flowsForSimulation(since: Date().addingTimeInterval(-7 * 86400)), + try $0.loadPolicies()) + } + guard candidate == draft.configuration, + packVersion == packs.version, + ruleVersion == rules.version else { return } + preview = RulePackSimulation.run(candidate: candidate, definitions: packs.definitions, + configurations: packs.configurations, + overrides: packs.overrides, rules: rules.rules, + policies: input.1, paused: rules.isPaused, + flows: input.0, session: RuleStore.session) + } catch { + guard candidate == draft.configuration, + packVersion == packs.version, + ruleVersion == rules.version else { return } + previewError = L("Failed: %@", L("Test against recent traffic")) } - preview = RulePackSimulation.run(candidate: draft.configuration, definitions: packs.definitions, - configurations: packs.configurations, - overrides: packs.overrides, rules: monitor.rules.rules, - policies: input?.1 ?? [:], paused: monitor.rules.isPaused, - flows: input?.0 ?? [], session: RuleStore.session) } } diff --git a/FlowlightTests/RulePackTests.swift b/FlowlightTests/RulePackTests.swift index b7e133e5..cfaa4bee 100644 --- a/FlowlightTests/RulePackTests.swift +++ b/FlowlightTests/RulePackTests.swift @@ -363,7 +363,7 @@ final class RulePackTests: XCTestCase { XCTAssertFalse(draft.configuration.enabled) XCTAssertEqual(draft.configuration.scope, .allApps) - XCTAssertNil(draft.saveAction) + XCTAssertEqual(draft.requestSave(), .commit(saved)) } func testEditorActivationAndCancelNeverMutateSavedConfiguration() { @@ -387,12 +387,40 @@ final class RulePackTests: XCTestCase { XCTAssertEqual(draft.submitApp(installed: []), .added("com.apple.curl")) XCTAssertEqual(draft.configuration.apps, ["com.apple.curl"]) XCTAssertEqual(draft.typedApp, "") - XCTAssertNil(draft.saveAction, "Return in the app field must not save or dismiss the editor") draft.typedApp = "com.example.second" XCTAssertEqual(draft.submitApp(installed: []), .added("com.example.second")) XCTAssertEqual(draft.configuration.apps, ["com.apple.curl", "com.example.second"]) - XCTAssertNil(draft.saveAction, "Return must remain Add even after Save becomes valid") + XCTAssertEqual(draft.requestSave(), .commit(draft.configuration), + "Return only edits the draft; explicit Save decides whether to commit") + } + + func testConcurrentCardSavesStayTrackedIndependently() { + var saves = RulePackSaveTracker() + saves.begin("official.advertising") + saves.begin("official.tracking") + + saves.finish("official.advertising") + + XCTAssertFalse(saves.contains("official.advertising")) + XCTAssertTrue(saves.contains("official.tracking")) + } + + func testEditorWaitsForInstalledAppsBeforeResolvingDisplayName() { + let safari = InstalledApps.App(name: "Safari", bundleID: "com.apple.Safari", + path: "/Applications/Safari.app") + var draft = RulePackEditorDraft(saved: RulePackConfiguration(packID: "official.advertising"), + intent: .activate) + draft.configuration.scope = .selectedApps + draft.typedApp = "Safari" + + XCTAssertEqual(draft.submitApp(installed: [], appsLoaded: false), .loading) + XCTAssertEqual(draft.typedApp, "Safari") + XCTAssertTrue(draft.configuration.apps.isEmpty) + + XCTAssertEqual(draft.submitApp(installed: [safari], appsLoaded: true), .added("com.apple.Safari")) + XCTAssertEqual(draft.configuration.apps, ["com.apple.Safari"]) + XCTAssertEqual(draft.typedApp, "") } func testEditorExplicitSaveAndBroadeningDecision() { @@ -405,8 +433,8 @@ final class RulePackTests: XCTestCase { XCTAssertEqual(draft.requestSave(), .confirmBroadening) selected.apps = [] - var invalid = RulePackEditorDraft(saved: selected, intent: .review) - XCTAssertNil(invalid.saveAction) + let invalid = RulePackEditorDraft(saved: selected, intent: .review) + XCTAssertNil(invalid.requestSave()) XCTAssertFalse(invalid.canSave) } From 54bc6dc695f247cdf0ea401d576ea552593784fa Mon Sep 17 00:00:00 2001 From: blessdyb Date: Sun, 18 Oct 2026 08:39:28 -0700 Subject: [PATCH 4/5] Keep pack save failures visible Co-Authored-By: Claude Code --- Flowlight/UI/RulePacksView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flowlight/UI/RulePacksView.swift b/Flowlight/UI/RulePacksView.swift index 4d4a0cd9..26d92597 100644 --- a/Flowlight/UI/RulePacksView.swift +++ b/Flowlight/UI/RulePacksView.swift @@ -49,7 +49,6 @@ private struct RulePacksContent: View { RulePackEditor(pack: request.pack, saved: packs.configuration(for: request.pack.id), intent: request.intent, packs: packs, rules: monitor.rules) } - .onChange(of: packs.version) { _, _ in saveErrors.removeAll() } } private var explanation: some View { @@ -79,6 +78,7 @@ private struct RulePacksContent: View { Spacer(minLength: 8) Toggle("", isOn: Binding(get: { configuration.enabled }, set: { enabled in if enabled { + saveErrors[pack.id] = nil editor = RulePackEditorRequest(pack: pack, intent: .activate) } else { var changed = configuration @@ -102,6 +102,7 @@ private struct RulePacksContent: View { } Spacer() Button(L("Review…")) { + saveErrors[pack.id] = nil editor = RulePackEditorRequest(pack: pack, intent: .review) }.disabled(isSaving) } From 8555829331663e390c3a86ec0f6d4a8afc4632ed Mon Sep 17 00:00:00 2001 From: blessdyb Date: Sun, 18 Oct 2026 15:29:23 -0700 Subject: [PATCH 5/5] Release Flowlight 0.15.2 Repair managed-pack activity schemas by verifying the physical SQLite key, keep app discovery and exception persistence truthful under partial failures, and publish the 0.15.2 release metadata. Co-Authored-By: Claude Code --- Flowlight/Analysis/InstalledApps.swift | 67 +++++-- Flowlight/Analysis/RulePackStore.swift | 40 ++-- Flowlight/Storage/TrafficDatabase.swift | 43 +++-- Flowlight/UI/RuleEditor.swift | 6 +- Flowlight/UI/RulePackEditorDraft.swift | 11 +- Flowlight/UI/RulePacksView.swift | 56 ++++-- Flowlight/UI/RulesView.swift | 14 +- FlowlightTests/InstalledAppsTests.swift | 50 +++++ FlowlightTests/RulePackTests.swift | 233 +++++++++++++++++++++++- docs/404.html | 2 +- docs/about/index.html | 2 +- docs/de/about/index.html | 2 +- docs/de/docs/index.html | 4 +- docs/de/index.html | 6 +- docs/de/privacy/index.html | 2 +- docs/de/threat-model/index.html | 2 +- docs/docs/index.html | 4 +- docs/es/about/index.html | 2 +- docs/es/docs/index.html | 4 +- docs/es/index.html | 6 +- docs/es/privacy/index.html | 2 +- docs/es/threat-model/index.html | 2 +- docs/fr/about/index.html | 2 +- docs/fr/docs/index.html | 4 +- docs/fr/index.html | 6 +- docs/fr/privacy/index.html | 2 +- docs/fr/threat-model/index.html | 2 +- docs/index.html | 6 +- docs/it/about/index.html | 2 +- docs/it/docs/index.html | 4 +- docs/it/index.html | 6 +- docs/it/privacy/index.html | 2 +- docs/it/threat-model/index.html | 2 +- docs/ja/about/index.html | 2 +- docs/ja/docs/index.html | 4 +- docs/ja/index.html | 6 +- docs/ja/privacy/index.html | 2 +- docs/ja/threat-model/index.html | 2 +- docs/ko/about/index.html | 2 +- docs/ko/docs/index.html | 4 +- docs/ko/index.html | 6 +- docs/ko/privacy/index.html | 2 +- docs/ko/threat-model/index.html | 2 +- docs/llms-full.txt | 20 +- docs/llms.txt | 2 +- docs/plugins/index.html | 2 +- docs/privacy/index.html | 2 +- docs/pt-PT/about/index.html | 2 +- docs/pt-PT/docs/index.html | 4 +- docs/pt-PT/index.html | 6 +- docs/pt-PT/privacy/index.html | 2 +- docs/pt-PT/threat-model/index.html | 2 +- docs/releases/index.html | 18 +- docs/threat-model/index.html | 2 +- docs/zh-Hans/about/index.html | 2 +- docs/zh-Hans/docs/index.html | 4 +- docs/zh-Hans/index.html | 6 +- docs/zh-Hans/privacy/index.html | 2 +- docs/zh-Hans/threat-model/index.html | 2 +- docs/zh-Hant/about/index.html | 2 +- docs/zh-Hant/docs/index.html | 4 +- docs/zh-Hant/index.html | 6 +- docs/zh-Hant/privacy/index.html | 2 +- docs/zh-Hant/threat-model/index.html | 2 +- project.yml | 4 +- site/pages/releases.html | 16 +- 66 files changed, 588 insertions(+), 156 deletions(-) diff --git a/Flowlight/Analysis/InstalledApps.swift b/Flowlight/Analysis/InstalledApps.swift index 4fc96db3..05319f40 100644 --- a/Flowlight/Analysis/InstalledApps.swift +++ b/Flowlight/Analysis/InstalledApps.swift @@ -22,52 +22,81 @@ enum InstalledApps { var complete: Bool } - private static let lock = NSLock() + private static let condition = NSCondition() private static var cached: ScanResult? + private static var scanning = false /// Where applications live. The system folder is included because Safari and Mail are perfectly reasonable /// things to focus on, and they aren't in /Applications. private static var searchPaths: [String] { - ["/Applications", "/Applications/Utilities", "/System/Applications", "/System/Applications/Utilities", - NSHomeDirectory() + "/Applications"] + ["/Applications", "/System/Applications", NSHomeDirectory() + "/Applications"] } - /// Scanned once and kept. A Mac doesn't gain applications while a popover is open, and re-reading several - /// hundred Info.plists on every keystroke would be felt. + /// A complete catalog is retained for synchronous lookups. Callers that can be first to ask load off the main + /// actor; a partial scan is returned to that caller but never poisons every later lookup. static func all() -> [App] { scan().apps } - static func scan() -> ScanResult { - lock.lock() - if let cached { lock.unlock(); return cached } - lock.unlock() + static func scan(refresh: Bool = false) -> ScanResult { + cachedScan(refresh: refresh) { scan(in: searchPaths) } + } + + static func cachedScan(refresh: Bool = false, _ load: () -> ScanResult) -> ScanResult { + condition.lock() + if !refresh, let cached { condition.unlock(); return cached } + if scanning { + repeat { condition.wait() } while scanning + if let cached { condition.unlock(); return cached } + } + scanning = true + condition.unlock() + let result = load() + condition.lock() + if result.complete { cached = result } + scanning = false + condition.broadcast() + condition.unlock() + return result + } + + static func resetCacheForTesting() { + condition.lock() + cached = nil + condition.unlock() + } + + static func scan(in searchPaths: [String], manager: FileManager = .default) -> ScanResult { var found: [String: App] = [:] var complete = true - let manager = FileManager.default for directory in searchPaths { guard manager.fileExists(atPath: directory) else { continue } - guard let entries = try? manager.contentsOfDirectory(atPath: directory) else { + guard let enumerator = manager.enumerator( + at: URL(fileURLWithPath: directory), + includingPropertiesForKeys: [.isDirectoryKey, .isPackageKey], + options: [.skipsHiddenFiles], + errorHandler: { _, _ in complete = false; return true } + ) else { complete = false continue } - for entry in entries where entry.hasSuffix(".app") { - let path = directory + "/" + entry - guard let bundle = Bundle(path: path), let id = bundle.bundleIdentifier, !id.isEmpty else { continue } + for case let url as URL in enumerator { + guard url.pathExtension.caseInsensitiveCompare("app") == .orderedSame else { continue } + enumerator.skipDescendants() + guard let bundle = Bundle(url: url), let id = bundle.bundleIdentifier, !id.isEmpty else { + continue + } let name = (bundle.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String) ?? (bundle.object(forInfoDictionaryKey: "CFBundleName") as? String) - ?? String(entry.dropLast(4)) + ?? url.deletingPathExtension().lastPathComponent let purpose = (bundle.object(forInfoDictionaryKey: "NSBluetoothAlwaysUsageDescription") as? String) ?? (bundle.object(forInfoDictionaryKey: "NSBluetoothPeripheralUsageDescription") as? String) - found[id] = App(name: name, bundleID: id, path: path, bluetoothPurpose: purpose) + found[id] = App(name: name, bundleID: id, path: url.path, bluetoothPurpose: purpose) } } let result = ScanResult( apps: found.values.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }, complete: complete && !found.isEmpty ) - if result.complete { - lock.lock(); cached = result; lock.unlock() - } return result } diff --git a/Flowlight/Analysis/RulePackStore.swift b/Flowlight/Analysis/RulePackStore.swift index acede341..0162cfd8 100644 --- a/Flowlight/Analysis/RulePackStore.swift +++ b/Flowlight/Analysis/RulePackStore.swift @@ -1,5 +1,14 @@ import Foundation +extension RulePackOverride { + func hasSameScope(as other: RulePackOverride) -> Bool { + packID == other.packID + && app.caseInsensitiveCompare(other.app) == .orderedSame + && host.caseInsensitiveCompare(other.host) == .orderedSame + && match == other.match + } +} + /// The app-owned half of managed rule packs: built-in definitions, user activation/scope, exceptions, and the /// complete validated snapshot handed to the Network Extension. @MainActor @@ -110,25 +119,34 @@ final class RulePackStore: ObservableObject { save(value) } - func addOverride(_ override: RulePackOverride) { + func addOverride(_ override: RulePackOverride, completion: ((Bool) -> Void)? = nil) { guard definitions.contains(where: { $0.id == override.packID }), - let host = HostCanonicalizer.host(override.host) else { return } + let host = HostCanonicalizer.host(override.host) else { + completion?(false) + return + } var cleaned = override cleaned.host = host cleaned.app = override.app.trimmingCharacters(in: .whitespacesAndNewlines) - persist({ try $0.saveRulePackOverride(cleaned) }) { [weak self] in - guard let self else { return } - self.overrides.removeAll { $0.id == cleaned.id } + persist({ try $0.saveRulePackOverride(cleaned) }, then: { [weak self] in + guard let self else { completion?(false); return } + self.overrides.removeAll { $0.id == cleaned.id || $0.hasSameScope(as: cleaned) } self.overrides.append(cleaned) self.publish() - } + completion?(true) + }, onFailure: { + completion?(false) + }) } - func deleteOverride(_ override: RulePackOverride) { - persist({ try $0.deleteRulePackOverride(override.id) }) { [weak self] in - self?.overrides.removeAll { $0.id == override.id } + func deleteOverride(_ override: RulePackOverride, completion: ((Bool) -> Void)? = nil) { + persist({ try $0.deleteRulePackOverride(override) }, then: { [weak self] in + self?.overrides.removeAll { $0.hasSameScope(as: override) } self?.publish() - } + completion?(true) + }, onFailure: { + completion?(false) + }) } func updateStatus(_ newStatus: ApplyStatus) { status = newStatus } @@ -201,7 +219,7 @@ final class RulePackStore: ObservableObject { Task { @MainActor in commit() } } catch { Task { @MainActor in - self?.status = .failed("Could not save the rule pack change: \(error.localizedDescription)") + self?.status = .failed("Could not save the rule pack change: \(String(describing: error))") onFailure?() } } diff --git a/Flowlight/Storage/TrafficDatabase.swift b/Flowlight/Storage/TrafficDatabase.swift index b24939c4..cd7a82d9 100644 --- a/Flowlight/Storage/TrafficDatabase.swift +++ b/Flowlight/Storage/TrafficDatabase.swift @@ -225,6 +225,11 @@ struct RulePackEventRecord: Identifiable, Sendable, Hashable { /// flows_1s → agg_1m (complete minutes) → agg_1h / agg_1d (folded from immutable minute rows). final class TrafficDatabase: @unchecked Sendable { static let tables = ["flows_1s", "agg_1m", "agg_1h", "agg_1d"] + private static let rulePackEventKey = [ + "bucket", "pack_id", "pack_version", "entry_id", "category", "match", "action", + "bundle_id", "app_name", "agent", "agent_name", "host", "matched_host", "port", + ] + private static let rulePackEventKeySQL = rulePackEventKey.joined(separator: ", ") let queue = DispatchQueue(label: "flowlight.db", qos: .userInitiated) private let conn: SQLiteConnection let url: URL @@ -490,8 +495,7 @@ final class TrafficDatabase: @unchecked Sendable { count INTEGER NOT NULL, first_at INTEGER NOT NULL, last_at INTEGER NOT NULL, bundle_id TEXT NOT NULL, app_name TEXT NOT NULL, agent TEXT NOT NULL, agent_name TEXT NOT NULL, host TEXT NOT NULL, matched_host TEXT NOT NULL, port INTEGER NOT NULL, - UNIQUE(bucket, pack_id, pack_version, entry_id, category, match, action, bundle_id, app_name, - agent, agent_name, host, matched_host, port)); + UNIQUE(\(Self.rulePackEventKeySQL))); CREATE TABLE IF NOT EXISTS rule_pack_event_watermarks ( delivery_session TEXT PRIMARY KEY, sequence INTEGER NOT NULL, received INTEGER NOT NULL, dropped_hits INTEGER NOT NULL, dropped_keys INTEGER NOT NULL); @@ -502,7 +506,16 @@ final class TrafficDatabase: @unchecked Sendable { "SELECT watermark FROM rollup_state WHERE tier = 'schema.rulePackEventKey'", map: { $0.int(0) } ).first ?? 0 - if packEventKeyVersion < 2 { + let packEventIndexes = try conn.query("PRAGMA index_list(rule_pack_event_rollups)") { + (name: $0.text(1), unique: $0.int(2) == 1, partial: $0.int(4) == 1) + } + let hasCurrentPackEventKey = try packEventIndexes + .filter { $0.unique && !$0.partial } + .contains { index in + let quoted = index.name.replacingOccurrences(of: "'", with: "''") + return try conn.query("PRAGMA index_info('\(quoted)')") { $0.text(2) } == Self.rulePackEventKey + } + if packEventKeyVersion < 2 || !hasCurrentPackEventKey { let oldPackEventColumns = Set(try conn.query("PRAGMA table_info(rule_pack_event_rollups)") { $0.text(1) }) let oldMatch = oldPackEventColumns.contains("match") ? "match" : "'suffix'" try conn.transaction { @@ -1056,14 +1069,23 @@ final class TrafficDatabase: @unchecked Sendable { func saveRulePackOverride(_ override: RulePackOverride) throws { let json = String(decoding: try JSONEncoder().encode(override), as: UTF8.self) - try conn.run(""" - INSERT INTO rule_pack_overrides (id, override, updated) VALUES (?,?,?) - ON CONFLICT(id) DO UPDATE SET override = excluded.override, updated = excluded.updated - """, [.text(override.id.uuidString), .text(json), .int(Int64(Date().timeIntervalSince1970))]) + try conn.transaction { + for existing in try loadRulePackOverrides().filter({ $0.id != override.id && $0.hasSameScope(as: override) }) { + try conn.run("DELETE FROM rule_pack_overrides WHERE id = ?", [.text(existing.id.uuidString)]) + } + try conn.run(""" + INSERT INTO rule_pack_overrides (id, override, updated) VALUES (?,?,?) + ON CONFLICT(id) DO UPDATE SET override = excluded.override, updated = excluded.updated + """, [.text(override.id.uuidString), .text(json), .int(Int64(Date().timeIntervalSince1970))]) + } } - func deleteRulePackOverride(_ id: UUID) throws { - try conn.run("DELETE FROM rule_pack_overrides WHERE id = ?", [.text(id.uuidString)]) + func deleteRulePackOverride(_ override: RulePackOverride) throws { + try conn.transaction { + for existing in try loadRulePackOverrides().filter({ $0.hasSameScope(as: override) }) { + try conn.run("DELETE FROM rule_pack_overrides WHERE id = ?", [.text(existing.id.uuidString)]) + } + } } /// Commits a retryable extension delivery exactly once. One monotonic watermark per extension process and the @@ -1103,8 +1125,7 @@ final class TrafficDatabase: @unchecked Sendable { (bucket, pack_id, pack_version, entry_id, category, match, action, count, first_at, last_at, bundle_id, app_name, agent, agent_name, host, matched_host, port) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) - ON CONFLICT(bucket, pack_id, pack_version, entry_id, category, match, action, bundle_id, app_name, - agent, agent_name, host, matched_host, port) DO UPDATE SET + ON CONFLICT(\(Self.rulePackEventKeySQL)) DO UPDATE SET count = count + excluded.count, first_at = MIN(first_at, excluded.first_at), last_at = MAX(last_at, excluded.last_at) diff --git a/Flowlight/UI/RuleEditor.swift b/Flowlight/UI/RuleEditor.swift index 80e97de8..0218ae4a 100644 --- a/Flowlight/UI/RuleEditor.swift +++ b/Flowlight/UI/RuleEditor.swift @@ -12,8 +12,7 @@ struct RuleEditor: View { @Environment(\.dismiss) private var dismiss @State private var appQuery = "" @State private var showAdvanced = false - - private var apps: [InstalledApps.App] { InstalledApps.all() } + @State private var apps: [InstalledApps.App] = [] var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -135,6 +134,9 @@ struct RuleEditor: View { .padding(16) .frame(width: 540) .frame(minHeight: 520) + .task { + apps = await Task.detached(priority: .userInitiated) { InstalledApps.all() }.value + } } /// The rule, read back, above the fields that make it. An unfinished rule says so here rather than only at the diff --git a/Flowlight/UI/RulePackEditorDraft.swift b/Flowlight/UI/RulePackEditorDraft.swift index 92873225..ca31f83c 100644 --- a/Flowlight/UI/RulePackEditorDraft.swift +++ b/Flowlight/UI/RulePackEditorDraft.swift @@ -38,8 +38,15 @@ struct RulePackEditorDraft { } mutating func addApp(_ value: String, installed: [InstalledApps.App], appsLoaded: Bool = true) -> AppSubmission { - guard appsLoaded else { return .loading } - guard let target = InstalledApps.target(forTyped: value, apps: installed) else { return .ignored } + let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) + let exactApp = installed.contains { + $0.bundleID.caseInsensitiveCompare(trimmed) == .orderedSame + || $0.name.caseInsensitiveCompare(trimmed) == .orderedSame + } + let explicitIdentity = trimmed.contains(".") && !trimmed.contains(where: \.isWhitespace) + || AgentCatalog.knownAgent(bundleID: trimmed, appName: trimmed) != nil + guard appsLoaded || exactApp || explicitIdentity else { return .loading } + guard let target = InstalledApps.target(forTyped: trimmed, apps: installed) else { return .ignored } if !configuration.apps.contains(where: { $0.caseInsensitiveCompare(target.value) == .orderedSame }) { configuration.apps.append(target.value) configuration.apps.sort() diff --git a/Flowlight/UI/RulePacksView.swift b/Flowlight/UI/RulePacksView.swift index 26d92597..23d01f3e 100644 --- a/Flowlight/UI/RulePacksView.swift +++ b/Flowlight/UI/RulePacksView.swift @@ -226,6 +226,7 @@ private struct RulePackEditor: View { .onChange(of: draft.configuration) { _, _ in invalidatePreview() } .onChange(of: packs.version) { _, _ in invalidatePreview() } .onChange(of: rules.version) { _, _ in invalidatePreview() } + .onChange(of: monitor.dataVersion) { _, _ in invalidatePreview() } .onAppear { loadApps() } .confirmationDialog(L("Apply this pack to every app?"), isPresented: $confirmBroadening) { Button(L("Apply to every app"), role: .destructive) { commit(draft.configuration) } @@ -245,14 +246,14 @@ private struct RulePackEditor: View { if draft.configuration.scope == .selectedApps { HStack { TextField(L("App name, bundle identifier, or process"), text: $draft.typedApp) - .onSubmit { if appsLoaded { addApp() } } + .onSubmit { addApp() } if !appsScanned { ProgressView().controlSize(.small) } else if !appsLoaded { Button(L("Refresh")) { loadApps() }.controlSize(.small) } Button(L("Add")) { addApp() } - .disabled(!appsLoaded || draft.typedApp.trimmingCharacters(in: .whitespaces).isEmpty) + .disabled(draft.typedApp.trimmingCharacters(in: .whitespaces).isEmpty) } if !draft.typedApp.isEmpty, !appSuggestions.isEmpty { VStack(alignment: .leading, spacing: 0) { @@ -336,7 +337,16 @@ private struct RulePackEditor: View { Text("→").foregroundStyle(.tertiary) Text(exception.host).font(.caption.monospaced()) Spacer() - Button(L("Delete"), role: .destructive) { packs.deleteOverride(exception) } + Button(L("Delete"), role: .destructive) { + saving = true + saveError = nil + packs.deleteOverride(exception) { success in + saving = false + if !success { + saveError = packs.failureMessage ?? L("Failed: %@", L("Delete")) + } + } + } }.padding(.vertical, 2) } } @@ -350,10 +360,11 @@ private struct RulePackEditor: View { } private func loadApps() { + let refresh = appsScanned appsScanned = false appsLoaded = false Task { - let result = await Task.detached(priority: .userInitiated) { InstalledApps.scan() }.value + let result = await Task.detached(priority: .userInitiated) { InstalledApps.scan(refresh: refresh) }.value installed = result.apps appsLoaded = result.complete appsScanned = true @@ -397,6 +408,7 @@ private struct RulePackEditor: View { let candidate = draft.configuration let packVersion = packs.version let ruleVersion = rules.version + let dataVersion = monitor.dataVersion previewing = true preview = nil previewError = nil @@ -408,7 +420,8 @@ private struct RulePackEditor: View { } guard candidate == draft.configuration, packVersion == packs.version, - ruleVersion == rules.version else { return } + ruleVersion == rules.version, + dataVersion == monitor.dataVersion else { return } preview = RulePackSimulation.run(candidate: candidate, definitions: packs.definitions, configurations: packs.configurations, overrides: packs.overrides, rules: rules.rules, @@ -417,8 +430,9 @@ private struct RulePackEditor: View { } catch { guard candidate == draft.configuration, packVersion == packs.version, - ruleVersion == rules.version else { return } - previewError = L("Failed: %@", L("Test against recent traffic")) + ruleVersion == rules.version, + dataVersion == monitor.dataVersion else { return } + previewError = L("Failed: %@", String(describing: error)) } } } @@ -428,6 +442,8 @@ struct RulePackEventRow: View { @EnvironmentObject var monitor: TrafficMonitor @EnvironmentObject var nav: AppNavigation @State private var recentOverride: RulePackOverride? + @State private var overrideSaving = false + @State private var overrideError: String? var body: some View { HStack(alignment: .top, spacing: 10) { @@ -444,9 +460,14 @@ struct RulePackEventRow: View { Text(event.lastAt.formatted(date: .omitted, time: .standard)).font(.caption.monospacedDigit()).foregroundStyle(.secondary) if let recentOverride { Button(L("Undo")) { - monitor.rulePacks.deleteOverride(recentOverride) - self.recentOverride = nil - }.buttonStyle(.link).font(.caption) + overrideSaving = true + overrideError = nil + monitor.rulePacks.deleteOverride(recentOverride) { success in + overrideSaving = false + if success { self.recentOverride = nil } + else { overrideError = monitor.rulePacks.failureMessage ?? L("Failed: %@", L("Delete")) } + } + }.buttonStyle(.link).font(.caption).disabled(overrideSaving) } else { Menu { Button(L("Allow for %@", event.appName)) { allowForApp() } @@ -456,7 +477,11 @@ struct RulePackEventRow: View { } label: { Text(L("Allow…")) } - .menuStyle(.borderlessButton).fixedSize().font(.caption) + .menuStyle(.borderlessButton).fixedSize().font(.caption).disabled(overrideSaving) + } + if overrideSaving { ProgressView().controlSize(.mini) } + if let overrideError { + Text(overrideError).font(.caption2).foregroundStyle(FL.critical) } } }.padding(.horizontal, 10).padding(.vertical, 8) @@ -465,8 +490,13 @@ struct RulePackEventRow: View { private func allowForApp() { let exception = RulePackOverride(packID: event.packID, app: event.bundleID, host: event.matchedHost, match: event.match) - recentOverride = exception - monitor.rulePacks.addOverride(exception) + overrideSaving = true + overrideError = nil + monitor.rulePacks.addOverride(exception) { success in + overrideSaving = false + if success { recentOverride = exception } + else { overrideError = monitor.rulePacks.failureMessage ?? L("Failed: %@", L("Save")) } + } } private var packName: String { diff --git a/Flowlight/UI/RulesView.swift b/Flowlight/UI/RulesView.swift index 33ed64e7..16ba75c2 100644 --- a/Flowlight/UI/RulesView.swift +++ b/Flowlight/UI/RulesView.swift @@ -693,6 +693,7 @@ struct RuleTemplateLibrary: View { @State private var app = "" @State private var destination = "" @State private var added: Set = [] + @State private var installed: [InstalledApps.App] = [] /// The subject as the cards see it. The readable name is looked up rather than stored, so typing over the app /// can't leave a rule named after the one that was there before. @@ -704,8 +705,7 @@ struct RuleTemplateLibrary: View { if bundleID.caseInsensitiveCompare(subject.app) == .orderedSame, !subject.appName.isEmpty { return subject.appName } - return InstalledApps.all().first { $0.bundleID.caseInsensitiveCompare(bundleID) == .orderedSame }?.name - ?? bundleID + return installed.first { $0.bundleID.caseInsensitiveCompare(bundleID) == .orderedSame }?.name ?? bundleID } var body: some View { @@ -737,6 +737,9 @@ struct RuleTemplateLibrary: View { } .frame(width: Measure.prose) .frame(minHeight: 560) + .task { + installed = await Task.detached(priority: .userInitiated) { InstalledApps.all() }.value + } } private var header: some View { @@ -862,10 +865,9 @@ struct RuleTemplateLibrary: View { } private var suggestions: [InstalledApps.App] { - let all = InstalledApps.all() - guard !app.isEmpty, !all.contains(where: { $0.bundleID.caseInsensitiveCompare(app) == .orderedSame }) - else { return [] } - return Array(InstalledApps.search(app, in: all).prefix(4)) + guard !app.isEmpty, + !installed.contains(where: { $0.bundleID.caseInsensitiveCompare(app) == .orderedSame }) else { return [] } + return Array(InstalledApps.search(app, in: installed).prefix(4)) } /// Every rule, written out. Long lists are the whole point of a template, so they aren't truncated — an diff --git a/FlowlightTests/InstalledAppsTests.swift b/FlowlightTests/InstalledAppsTests.swift index 02509934..c265c6f8 100644 --- a/FlowlightTests/InstalledAppsTests.swift +++ b/FlowlightTests/InstalledAppsTests.swift @@ -33,6 +33,56 @@ final class InstalledAppsTests: XCTestCase { func testBlankInputIsRefused() { XCTAssertNil(InstalledApps.target(forTyped: " ", apps: apps)) } + + func testPartialScanDoesNotPoisonTheCompleteCache() { + InstalledApps.resetCacheForTesting() + defer { InstalledApps.resetCacheForTesting() } + let partial = InstalledApps.ScanResult(apps: [apps[0]], complete: false) + let complete = InstalledApps.ScanResult(apps: apps, complete: true) + var loads = 0 + + XCTAssertEqual(InstalledApps.cachedScan { loads += 1; return partial }.apps, [apps[0]]) + XCTAssertEqual(InstalledApps.cachedScan { loads += 1; return complete }.apps, apps) + XCTAssertEqual(InstalledApps.cachedScan { loads += 1; return partial }.apps, apps) + XCTAssertEqual(loads, 2) + } + + func testMalformedNestedAppDoesNotMakeTheCatalogIncomplete() throws { + let root = FileManager.default.temporaryDirectory.appendingPathComponent("installed-apps-broken-\(UUID().uuidString)") + let valid = root.appendingPathComponent("Setapp/Sample.app/Contents") + let broken = root.appendingPathComponent("Setapp/Broken.app") + defer { try? FileManager.default.removeItem(at: root) } + try FileManager.default.createDirectory(at: valid, withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: broken, withIntermediateDirectories: true) + let plist: [String: Any] = ["CFBundleIdentifier": "com.example.Sample", "CFBundleName": "Sample"] + let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0) + try data.write(to: valid.appendingPathComponent("Info.plist")) + + let result = InstalledApps.scan(in: [root.path]) + + XCTAssertTrue(result.complete) + XCTAssertEqual(result.apps.map(\.bundleID), ["com.example.Sample"]) + } + + func testScanFindsAppsBelowCollectionDirectories() throws { + let root = FileManager.default.temporaryDirectory.appendingPathComponent("installed-apps-\(UUID().uuidString)") + let nested = root.appendingPathComponent("Setapp/Sample.app") + defer { try? FileManager.default.removeItem(at: root) } + try FileManager.default.createDirectory(at: nested, withIntermediateDirectories: true) + let plist: [String: Any] = [ + "CFBundleIdentifier": "com.example.Sample", + "CFBundleName": "Sample", + ] + let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0) + let contents = nested.appendingPathComponent("Contents") + try FileManager.default.createDirectory(at: contents, withIntermediateDirectories: true) + try data.write(to: contents.appendingPathComponent("Info.plist")) + + let result = InstalledApps.scan(in: [root.path]) + + XCTAssertTrue(result.complete) + XCTAssertEqual(result.apps.map(\.bundleID), ["com.example.Sample"]) + } } /// Focus is symmetric: either half alone is a complete focus. diff --git a/FlowlightTests/RulePackTests.swift b/FlowlightTests/RulePackTests.swift index cfaa4bee..ceebc79a 100644 --- a/FlowlightTests/RulePackTests.swift +++ b/FlowlightTests/RulePackTests.swift @@ -184,10 +184,113 @@ final class RulePackTests: XCTestCase { XCTAssertEqual(event?.matchedHost, "ads.example") XCTAssertEqual(event?.lastAt, Date(timeIntervalSince1970: 1_700_000_030)) XCTAssertEqual(events.first { $0.entryID == 8 }?.count, 4) - try reopened.deleteRulePackOverride(override.id) + try reopened.deleteRulePackOverride(override) XCTAssertTrue(try reopened.loadRulePackOverrides().isEmpty) } + func testStalePackEventMarkerRepairsThePhysicalUniqueKey() throws { + let url = FileManager.default.temporaryDirectory + .appendingPathComponent("flowlight-pack-stale-key-\(UUID().uuidString).sqlite") + defer { + for suffix in ["", "-wal", "-shm"] { + try? FileManager.default.removeItem(at: URL(fileURLWithPath: url.path + suffix)) + } + } + + do { + let fixture = try SQLiteConnection(path: url.path) + try fixture.execute(""" + CREATE TABLE rollup_state (tier TEXT PRIMARY KEY, watermark INTEGER NOT NULL); + INSERT INTO rollup_state (tier, watermark) VALUES ('schema.rulePackEventKey', 2); + CREATE TABLE rule_pack_event_rollups ( + id INTEGER PRIMARY KEY AUTOINCREMENT, bucket INTEGER NOT NULL, pack_id TEXT NOT NULL, + pack_version TEXT NOT NULL, entry_id INTEGER NOT NULL, category TEXT NOT NULL, + match TEXT NOT NULL DEFAULT 'suffix', action TEXT NOT NULL, + count INTEGER NOT NULL, first_at INTEGER NOT NULL, last_at INTEGER NOT NULL, + bundle_id TEXT NOT NULL, app_name TEXT NOT NULL, agent TEXT NOT NULL, agent_name TEXT NOT NULL, + host TEXT NOT NULL, matched_host TEXT NOT NULL, port INTEGER NOT NULL, + UNIQUE(bucket, pack_id, pack_version, entry_id, category, action, bundle_id, app_name, + agent, agent_name, host, matched_host, port)); + CREATE INDEX rule_pack_events_time ON rule_pack_event_rollups(bucket DESC); + CREATE INDEX rule_pack_events_pack ON rule_pack_event_rollups(pack_id, bucket DESC); + INSERT INTO rule_pack_event_rollups + (id, bucket, pack_id, pack_version, entry_id, category, match, action, count, + first_at, last_at, bundle_id, app_name, agent, agent_name, host, matched_host, port) + VALUES (41, 1700000000, 'official.ads', '1', 7, 'advertising', 'suffix', 'block', 3, + 1700000001, 1700000020, 'com.apple.Safari', 'Safari', 'com.apple.Safari', 'Safari', + 'cdn.ads.example', 'ads.example', 443); + """) + XCTAssertThrowsError(try fixture.run(""" + INSERT INTO rule_pack_event_rollups + (bucket, pack_id, pack_version, entry_id, category, match, action, count, + first_at, last_at, bundle_id, app_name, agent, agent_name, host, matched_host, port) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ON CONFLICT(bucket, pack_id, pack_version, entry_id, category, match, action, bundle_id, + app_name, agent, agent_name, host, matched_host, port) DO UPDATE SET + count = count + excluded.count + """, [.int(1_700_000_000), .text("official.ads"), .text("1"), .int(7), + .text("advertising"), .text("suffix"), .text("block"), .int(1), + .int(1_700_000_001), .int(1_700_000_020), .text("com.apple.Safari"), + .text("Safari"), .text("com.apple.Safari"), .text("Safari"), + .text("cdn.ads.example"), .text("ads.example"), .int(443)])) { error in + XCTAssertTrue(String(describing: error).contains("ON CONFLICT clause does not match")) + } + } + + var repaired: TrafficDatabase? = try TrafficDatabase(url: url) + let suffix = RulePackEvent( + bucket: 1_700_000_000, packID: "official.ads", packVersion: "1", entryID: 7, + category: .advertising, match: .suffix, action: .block, count: 2, + firstAt: 1_699_999_999, lastAt: 1_700_000_030, bundleID: "com.apple.Safari", + appName: "Safari", agentKey: "com.apple.Safari", agentName: "Safari", + host: "cdn.ads.example", matchedHost: "ads.example", port: 443) + var exact = suffix + exact.match = .exact + exact.count = 4 + try repaired?.recordRulePackEvents([suffix, exact]) + + var events = try XCTUnwrap(repaired).rulePackEvents() + XCTAssertEqual(events.count, 2) + XCTAssertEqual(events.first { $0.match == .suffix }?.id, 41) + XCTAssertEqual(events.first { $0.match == .suffix }?.count, 5) + XCTAssertEqual(events.first { $0.match == .suffix }?.firstAt, + Date(timeIntervalSince1970: 1_699_999_999)) + XCTAssertEqual(events.first { $0.match == .suffix }?.lastAt, + Date(timeIntervalSince1970: 1_700_000_030)) + XCTAssertEqual(events.first { $0.match == .exact }?.count, 4) + + let indexes = try XCTUnwrap(repaired).raw( + "PRAGMA index_list(rule_pack_event_rollups)", + map: { (name: $0.text(1), unique: $0.int(2), partial: $0.int(4)) } + ) + let expected = ["bucket", "pack_id", "pack_version", "entry_id", "category", "match", "action", + "bundle_id", "app_name", "agent", "agent_name", "host", "matched_host", "port"] + let matching = try indexes.filter { $0.unique == 1 && $0.partial == 0 }.contains { index in + let escaped = index.name.replacingOccurrences(of: "'", with: "''") + let columns = try XCTUnwrap(repaired).raw("PRAGMA index_info('\(escaped)')") { $0.text(2) } + return columns == expected + } + XCTAssertTrue(matching) + XCTAssertEqual(Set(indexes.map(\.name)).intersection(["rule_pack_events_time", "rule_pack_events_pack"]), + Set(["rule_pack_events_time", "rule_pack_events_pack"])) + XCTAssertEqual(try XCTUnwrap(repaired).raw( + "SELECT watermark FROM rollup_state WHERE tier = 'schema.rulePackEventKey'", map: { $0.int(0) } + ), [2]) + + repaired = nil + let reopened = try TrafficDatabase(url: url) + try reopened.recordRulePackEvents([suffix]) + let batch = RulePackEventBatch(events: [exact], droppedHits: 2, droppedKeys: 1) + XCTAssertTrue(try reopened.recordRulePackEventBatch(batch)) + XCTAssertFalse(try reopened.recordRulePackEventBatch(batch)) + XCTAssertEqual(try reopened.rulePackDeliveryStats(), RulePackDeliveryStats(droppedHits: 2, droppedKeys: 1)) + events = try reopened.rulePackEvents() + XCTAssertEqual(events.count, 2) + XCTAssertEqual(events.first { $0.match == .suffix }?.count, 7) + XCTAssertEqual(events.first { $0.match == .suffix }?.id, 41) + XCTAssertEqual(events.first { $0.match == .exact }?.count, 8) + } + @MainActor func testBuiltInPacksValidateAndDefaultOff() throws { XCTAssertNoThrow(try RulePackValidator.validate(BuiltInRulePacks.all)) @@ -275,6 +378,91 @@ final class RulePackTests: XCTestCase { try await waitUntil { completed } } + @MainActor + func testOverrideAppearsOnlyAfterDatabaseCommit() async throws { + let url = FileManager.default.temporaryDirectory + .appendingPathComponent("flowlight-pack-override-failure-\(UUID().uuidString).sqlite") + let db = try TrafficDatabase(url: url) + let store = RulePackStore() + store.attach(db: db) + try await waitUntil { store.configurations.count == store.definitions.count } + _ = try db.raw("DROP TABLE rule_pack_overrides") { _ in 0 } + + let exception = RulePackOverride(packID: "official.advertising", app: "com.apple.Safari", + host: "ads.example") + var result: Bool? + store.addOverride(exception) { result = $0 } + + try await waitUntil { result != nil } + XCTAssertEqual(result, false) + XCTAssertTrue(store.overrides.isEmpty) + XCTAssertTrue(store.failureMessage?.contains("no such table") == true) + } + + @MainActor + func testRepeatedOverrideReplacesAndDeleteRemovesItsWholeScope() async throws { + let url = FileManager.default.temporaryDirectory + .appendingPathComponent("flowlight-pack-override-dedupe-\(UUID().uuidString).sqlite") + let db = try TrafficDatabase(url: url) + let store = RulePackStore() + store.attach(db: db) + try await waitUntil { store.configurations.count == store.definitions.count } + + let first = RulePackOverride(packID: "official.advertising", app: "com.apple.Safari", + host: "ADS.EXAMPLE") + let second = RulePackOverride(packID: first.packID, app: "COM.APPLE.SAFARI", + host: "ads.example") + var added = 0 + store.addOverride(first) { if $0 { added += 1 } } + try await waitUntil { added == 1 } + store.addOverride(second) { if $0 { added += 1 } } + try await waitUntil { added == 2 } + + XCTAssertEqual(store.overrides, [second]) + XCTAssertEqual(try db.sync { try $0.loadRulePackOverrides() }, [second]) + + let legacyJSON = String(decoding: try JSONEncoder().encode(first), as: UTF8.self) + _ = try db.raw( + "INSERT INTO rule_pack_overrides (id, override, updated) VALUES (?,?,?)", + [.text(first.id.uuidString), .text(legacyJSON), .int(1)] + ) { _ in 0 } + XCTAssertEqual(try db.sync { try $0.loadRulePackOverrides() }.count, 2) + + var deleted: Bool? + store.deleteOverride(second) { deleted = $0 } + try await waitUntil { deleted != nil } + XCTAssertEqual(deleted, true) + XCTAssertTrue(store.overrides.isEmpty) + XCTAssertTrue(try db.sync { try $0.loadRulePackOverrides() }.isEmpty) + } + + @MainActor + func testOverrideRemainsAfterDatabaseDeleteFails() async throws { + let url = FileManager.default.temporaryDirectory + .appendingPathComponent("flowlight-pack-override-delete-failure-\(UUID().uuidString).sqlite") + let db = try TrafficDatabase(url: url) + let store = RulePackStore() + store.attach(db: db) + try await waitUntil { store.configurations.count == store.definitions.count } + + let exception = RulePackOverride(packID: "official.advertising", app: "com.apple.Safari", + host: "ads.example") + var added: Bool? + store.addOverride(exception) { added = $0 } + try await waitUntil { added != nil } + XCTAssertEqual(added, true) + XCTAssertEqual(store.overrides, [exception]) + + _ = try db.raw("DROP TABLE rule_pack_overrides") { _ in 0 } + var deleted: Bool? + store.deleteOverride(exception) { deleted = $0 } + + try await waitUntil { deleted != nil } + XCTAssertEqual(deleted, false) + XCTAssertEqual(store.overrides, [exception]) + XCTAssertTrue(store.failureMessage?.contains("no such table") == true) + } + @MainActor func testStorePublishesAfterTransientFailureRecovers() async throws { let url = FileManager.default.temporaryDirectory.appendingPathComponent("flowlight-pack-recovery-\(UUID().uuidString).sqlite") @@ -423,6 +611,49 @@ final class RulePackTests: XCTestCase { XCTAssertEqual(draft.typedApp, "") } + func testEditorAcceptsExplicitBundleIdentityDuringIncompleteScan() { + var draft = RulePackEditorDraft(saved: RulePackConfiguration(packID: "official.advertising"), + intent: .activate) + draft.configuration.scope = .selectedApps + + draft.typedApp = "com.apple.Safari" + XCTAssertEqual(draft.submitApp(installed: [], appsLoaded: false), .added("com.apple.Safari")) + XCTAssertEqual(draft.configuration.apps, ["com.apple.Safari"]) + } + + func testEditorIncompleteScanAcceptsKnownCommandLineAgent() { + var draft = RulePackEditorDraft(saved: RulePackConfiguration(packID: "official.advertising"), + intent: .activate) + draft.configuration.scope = .selectedApps + draft.typedApp = "claude" + + XCTAssertEqual(draft.submitApp(installed: [], appsLoaded: false), .added("claude")) + XCTAssertEqual(draft.configuration.apps, ["claude"]) + } + + func testEditorIncompleteScanDoesNotGuessLowercaseDisplayNameIsAProcess() { + var draft = RulePackEditorDraft(saved: RulePackConfiguration(packID: "official.advertising"), + intent: .activate) + draft.configuration.scope = .selectedApps + draft.typedApp = "zoom" + + XCTAssertEqual(draft.submitApp(installed: [], appsLoaded: false), .loading) + XCTAssertEqual(draft.typedApp, "zoom") + XCTAssertTrue(draft.configuration.apps.isEmpty) + } + + func testEditorIncompleteScanStillResolvesKnownDisplayName() { + let safari = InstalledApps.App(name: "Safari", bundleID: "com.apple.Safari", + path: "/System/Applications/Safari.app") + var draft = RulePackEditorDraft(saved: RulePackConfiguration(packID: "official.advertising"), + intent: .activate) + draft.configuration.scope = .selectedApps + draft.typedApp = "Safari" + + XCTAssertEqual(draft.submitApp(installed: [safari], appsLoaded: false), .added("com.apple.Safari")) + XCTAssertEqual(draft.configuration.apps, ["com.apple.Safari"]) + } + func testEditorExplicitSaveAndBroadeningDecision() { var selected = RulePackConfiguration(packID: "official.advertising", enabled: true, scope: .selectedApps, apps: ["com.apple.curl"]) diff --git a/docs/404.html b/docs/404.html index f58b62be..f2cf6db1 100644 --- a/docs/404.html +++ b/docs/404.html @@ -86,7 +86,7 @@

    That page isn't here

    Try the home page,

diff --git a/docs/about/index.html b/docs/about/index.html index 66e89b79..acc27376 100644 --- a/docs/about/index.html +++ b/docs/about/index.html @@ -153,7 +153,7 @@

Thanks

diff --git a/docs/de/about/index.html b/docs/de/about/index.html index 2ff7ac90..aa33d9ba 100644 --- a/docs/de/about/index.html +++ b/docs/de/about/index.html @@ -152,7 +152,7 @@

Dank

diff --git a/docs/de/docs/index.html b/docs/de/docs/index.html index da5346ff..4da1b4e3 100644 --- a/docs/de/docs/index.html +++ b/docs/de/docs/index.html @@ -76,7 +76,7 @@

Dokumentation

Flowlight benutzen

-

Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.15.1, macOS 15 oder neuer.

+

Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.15.2, macOS 15 oder neuer.

diff --git a/docs/de/index.html b/docs/de/index.html index 533c8140..45a0e0b1 100644 --- a/docs/de/index.html +++ b/docs/de/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

Sieh, was deine Apps im Netz tun.
brew install --cask xinbetween/tap/flowlight

-

v0.15.1macOS 15+UniversalGPL-3.0Keine Telemetrie

+

v0.15.2macOS 15+UniversalGPL-3.0Keine Telemetrie

@@ -571,7 +571,7 @@

Wisse, was deinen Mac verlässt.

diff --git a/docs/de/privacy/index.html b/docs/de/privacy/index.html index b0d8e609..85a37842 100644 --- a/docs/de/privacy/index.html +++ b/docs/de/privacy/index.html @@ -188,7 +188,7 @@

Kontakt

diff --git a/docs/de/threat-model/index.html b/docs/de/threat-model/index.html index 3bcc4a83..2fca5313 100644 --- a/docs/de/threat-model/index.html +++ b/docs/de/threat-model/index.html @@ -228,7 +228,7 @@

Eine Schwachstelle melden

diff --git a/docs/docs/index.html b/docs/docs/index.html index f841d88a..3d90b954 100644 --- a/docs/docs/index.html +++ b/docs/docs/index.html @@ -76,7 +76,7 @@

Documentation

Using Flowlight

-

Everything from the first launch to tuning the agent rules. Flowlight 0.15.1, macOS 15 or later.

+

Everything from the first launch to tuning the agent rules. Flowlight 0.15.2, macOS 15 or later.

diff --git a/docs/es/about/index.html b/docs/es/about/index.html index c860c5eb..328c0bb4 100644 --- a/docs/es/about/index.html +++ b/docs/es/about/index.html @@ -152,7 +152,7 @@

Agradecimientos

diff --git a/docs/es/docs/index.html b/docs/es/docs/index.html index b93b3f5b..0a5da550 100644 --- a/docs/es/docs/index.html +++ b/docs/es/docs/index.html @@ -76,7 +76,7 @@

Documentación

Usar Flowlight

-

Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.15.1, macOS 15 o posterior.

+

Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.15.2, macOS 15 o posterior.

diff --git a/docs/es/index.html b/docs/es/index.html index 74ba3f12..44db97cb 100644 --- a/docs/es/index.html +++ b/docs/es/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

Observa la actividad de red de tus apps.

brew install --cask xinbetween/tap/flowlight

-

v0.15.1macOS 15+UniversalGPL-3.0Sin telemetría

+

v0.15.2macOS 15+UniversalGPL-3.0Sin telemetría

@@ -571,7 +571,7 @@

Ten claro qué sale de tu Mac.

diff --git a/docs/es/privacy/index.html b/docs/es/privacy/index.html index dd16995b..1788b96c 100644 --- a/docs/es/privacy/index.html +++ b/docs/es/privacy/index.html @@ -188,7 +188,7 @@

Contacto

diff --git a/docs/es/threat-model/index.html b/docs/es/threat-model/index.html index 4f8bbb36..18dc4227 100644 --- a/docs/es/threat-model/index.html +++ b/docs/es/threat-model/index.html @@ -223,7 +223,7 @@

Informar de una vulnerabilidad

diff --git a/docs/fr/about/index.html b/docs/fr/about/index.html index bc3091bf..20cc068a 100644 --- a/docs/fr/about/index.html +++ b/docs/fr/about/index.html @@ -152,7 +152,7 @@

Remerciements

diff --git a/docs/fr/docs/index.html b/docs/fr/docs/index.html index 1e615d9f..5603c0f4 100644 --- a/docs/fr/docs/index.html +++ b/docs/fr/docs/index.html @@ -76,7 +76,7 @@

Documentation

Utiliser Flowlight

-

Tout, du premier lancement au réglage des règles des agents. Flowlight 0.15.1, macOS 15 ou version ultérieure.

+

Tout, du premier lancement au réglage des règles des agents. Flowlight 0.15.2, macOS 15 ou version ultérieure.

diff --git a/docs/fr/index.html b/docs/fr/index.html index 2b1cba85..b14fb5ec 100644 --- a/docs/fr/index.html +++ b/docs/fr/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

Voyez l’activité réseau de vos apps.

brew install --cask xinbetween/tap/flowlight

-

v0.15.1macOS 15+UniverselGPL-3.0Sans télémétrie

+

v0.15.2macOS 15+UniverselGPL-3.0Sans télémétrie

@@ -571,7 +571,7 @@

Sachez ce qui quitte votre Mac.

diff --git a/docs/fr/privacy/index.html b/docs/fr/privacy/index.html index f7248635..610e0de2 100644 --- a/docs/fr/privacy/index.html +++ b/docs/fr/privacy/index.html @@ -188,7 +188,7 @@

Contact

diff --git a/docs/fr/threat-model/index.html b/docs/fr/threat-model/index.html index 376378f9..2f44ba06 100644 --- a/docs/fr/threat-model/index.html +++ b/docs/fr/threat-model/index.html @@ -226,7 +226,7 @@

Signaler une vulnérabilité

diff --git a/docs/index.html b/docs/index.html index a66a9a77..8749ea4a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

See your apps' network activity.
brew install --cask xinbetween/tap/flowlight

-

v0.15.1macOS 15+UniversalGPL-3.0No telemetry

+

v0.15.2macOS 15+UniversalGPL-3.0No telemetry

@@ -592,7 +592,7 @@

Know what leaves your Mac.

diff --git a/docs/it/about/index.html b/docs/it/about/index.html index c3ef1346..ce0f91a1 100644 --- a/docs/it/about/index.html +++ b/docs/it/about/index.html @@ -152,7 +152,7 @@

Ringraziamenti

diff --git a/docs/it/docs/index.html b/docs/it/docs/index.html index 46707524..56956f05 100644 --- a/docs/it/docs/index.html +++ b/docs/it/docs/index.html @@ -76,7 +76,7 @@

Documentazione

Usare Flowlight

-

Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.15.1, macOS 15 o successivo.

+

Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.15.2, macOS 15 o successivo.

diff --git a/docs/it/index.html b/docs/it/index.html index e04dd8e7..0711f834 100644 --- a/docs/it/index.html +++ b/docs/it/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

Vedi la rete delle tue app.
Ca Metti una stella su GitHub

brew install --cask xinbetween/tap/flowlight

-

v0.15.1macOS 15+UniversaleGPL-3.0Nessuna telemetria

+

v0.15.2macOS 15+UniversaleGPL-3.0Nessuna telemetria

@@ -571,7 +571,7 @@

Sappi che cosa esce dal tuo Mac.

diff --git a/docs/it/privacy/index.html b/docs/it/privacy/index.html index 0e41922a..d1fb9c39 100644 --- a/docs/it/privacy/index.html +++ b/docs/it/privacy/index.html @@ -188,7 +188,7 @@

Contatti

diff --git a/docs/it/threat-model/index.html b/docs/it/threat-model/index.html index cd636f1a..509a9be9 100644 --- a/docs/it/threat-model/index.html +++ b/docs/it/threat-model/index.html @@ -224,7 +224,7 @@

Segnalare una vulnerabilità

diff --git a/docs/ja/about/index.html b/docs/ja/about/index.html index 254e1a95..4ddf6fa7 100644 --- a/docs/ja/about/index.html +++ b/docs/ja/about/index.html @@ -153,7 +153,7 @@

謝辞

diff --git a/docs/ja/docs/index.html b/docs/ja/docs/index.html index 9e64ff3d..686a43ff 100644 --- a/docs/ja/docs/index.html +++ b/docs/ja/docs/index.html @@ -76,7 +76,7 @@

ドキュメント

Flowlight の使い方

-

初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.15.1、macOS 15 以降。

+

初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.15.2、macOS 15 以降。

diff --git a/docs/ja/index.html b/docs/ja/index.html index f804e52d..b4aceb06 100644 --- a/docs/ja/index.html +++ b/docs/ja/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

アプリの通信が見える。
brew install --cask xinbetween/tap/flowlight

-

v0.15.1macOS 15+ユニバーサルGPL-3.0テレメトリなし

+

v0.15.2macOS 15+ユニバーサルGPL-3.0テレメトリなし

@@ -570,7 +570,7 @@

Mac から何が出ていくのかを知る。

diff --git a/docs/ja/privacy/index.html b/docs/ja/privacy/index.html index ca6c1cd0..3668a5c0 100644 --- a/docs/ja/privacy/index.html +++ b/docs/ja/privacy/index.html @@ -186,7 +186,7 @@

連絡先

diff --git a/docs/ja/threat-model/index.html b/docs/ja/threat-model/index.html index 49c128a7..39f4a27b 100644 --- a/docs/ja/threat-model/index.html +++ b/docs/ja/threat-model/index.html @@ -225,7 +225,7 @@

脆弱性を報告する

diff --git a/docs/ko/about/index.html b/docs/ko/about/index.html index bdb31f5e..9435da1c 100644 --- a/docs/ko/about/index.html +++ b/docs/ko/about/index.html @@ -153,7 +153,7 @@

감사

diff --git a/docs/ko/docs/index.html b/docs/ko/docs/index.html index 8abc02f2..8ffee09e 100644 --- a/docs/ko/docs/index.html +++ b/docs/ko/docs/index.html @@ -76,7 +76,7 @@

문서

Flowlight 사용하기

-

첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.15.1, macOS 15 이상.

+

첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.15.2, macOS 15 이상.

diff --git a/docs/ko/index.html b/docs/ko/index.html index 03b89a4d..c43cfc85 100644 --- a/docs/ko/index.html +++ b/docs/ko/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

앱이 무엇을 하는지 봅니다.

brew install --cask xinbetween/tap/flowlight

-

v0.15.1macOS 15+유니버설GPL-3.0텔레메트리 없음

+

v0.15.2macOS 15+유니버설GPL-3.0텔레메트리 없음

@@ -571,7 +571,7 @@

내 Mac에서 무엇이 나가는지 아세요.

diff --git a/docs/ko/privacy/index.html b/docs/ko/privacy/index.html index 09d889c6..38e1a682 100644 --- a/docs/ko/privacy/index.html +++ b/docs/ko/privacy/index.html @@ -189,7 +189,7 @@

문의

diff --git a/docs/ko/threat-model/index.html b/docs/ko/threat-model/index.html index a4e8f2d9..5feabfb2 100644 --- a/docs/ko/threat-model/index.html +++ b/docs/ko/threat-model/index.html @@ -223,7 +223,7 @@

취약점 신고하기

diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 5cc393a3..df734bf8 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -62,7 +62,7 @@ Documentation Using Flowlight - Everything from the first launch to tuning the agent rules. Flowlight 0.15.1, macOS 15 or later. + Everything from the first launch to tuning the agent rules. Flowlight 0.15.2, macOS 15 or later. On this page @@ -887,7 +887,7 @@ Understand your AI agents. brew install --cask xinbetween/tap/flowlightCopy - v0.15.1macOS 15+UniversalGPL-3.0No telemetry + v0.15.2macOS 15+UniversalGPL-3.0No telemetry connectionslive @@ -1617,9 +1617,23 @@ Releases Downloads, checksums and full notes for each version are on GitHub Releases. - 0.15.1 + 0.15.2 October 18, 2026Latest + Repair managed-pack activity on upgraded databases. Flowlight now verifies the physical + activity key during startup and atomically repairs stale schemas, preserving existing rows before any + blocked connection is recorded. + + Keep pack decisions distinct and exactly once. Exact and suffix matches retain separate + provenance, repeated decisions aggregate correctly, and retried extension deliveries do not double count. + + Keep editor feedback truthful under partial failures. Incomplete app scans still accept + explicit bundle identifiers, previews expire when traffic or policies change, and exceptions appear only after their + database write succeeds with a concrete error when it does not. + + 0.15.1 +October 18, 2026 + Keep managed-pack changes safely in review. Opening or cancelling an off pack no longer activates it, and configuration changes persist only after an explicit Save succeeds. diff --git a/docs/llms.txt b/docs/llms.txt index b6693510..ae2a4a64 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -1,6 +1,6 @@ # Flowlight -> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.15.1. +> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.15.2. - [Download Flowlight.dmg](https://github.com/xinbetween/flowlight/releases/latest/download/Flowlight.dmg) - [Source code](https://github.com/xinbetween/flowlight) diff --git a/docs/plugins/index.html b/docs/plugins/index.html index 0ba7b180..b7040ab0 100644 --- a/docs/plugins/index.html +++ b/docs/plugins/index.html @@ -211,7 +211,7 @@

Review checklist

diff --git a/docs/privacy/index.html b/docs/privacy/index.html index a9063d7a..961a5c5e 100644 --- a/docs/privacy/index.html +++ b/docs/privacy/index.html @@ -195,7 +195,7 @@

Contact

diff --git a/docs/pt-PT/about/index.html b/docs/pt-PT/about/index.html index 01d336ad..757a7047 100644 --- a/docs/pt-PT/about/index.html +++ b/docs/pt-PT/about/index.html @@ -152,7 +152,7 @@

Agradecimentos

diff --git a/docs/pt-PT/docs/index.html b/docs/pt-PT/docs/index.html index 8696733e..25539837 100644 --- a/docs/pt-PT/docs/index.html +++ b/docs/pt-PT/docs/index.html @@ -76,7 +76,7 @@

Documentação

Usar o Flowlight

-

Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.15.1, macOS 15 ou posterior.

+

Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.15.2, macOS 15 ou posterior.

diff --git a/docs/pt-PT/index.html b/docs/pt-PT/index.html index 70178b72..186e7986 100644 --- a/docs/pt-PT/index.html +++ b/docs/pt-PT/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

Veja a atividade de rede das suas apps.

brew install --cask xinbetween/tap/flowlight

-

v0.15.1macOS 15+UniversalGPL-3.0Sem telemetria

+

v0.15.2macOS 15+UniversalGPL-3.0Sem telemetria

@@ -571,7 +571,7 @@

Saiba o que sai do seu Mac.

diff --git a/docs/pt-PT/privacy/index.html b/docs/pt-PT/privacy/index.html index c8f83100..aad6d4a1 100644 --- a/docs/pt-PT/privacy/index.html +++ b/docs/pt-PT/privacy/index.html @@ -188,7 +188,7 @@

Contacto

diff --git a/docs/pt-PT/threat-model/index.html b/docs/pt-PT/threat-model/index.html index e98cb423..009768b6 100644 --- a/docs/pt-PT/threat-model/index.html +++ b/docs/pt-PT/threat-model/index.html @@ -225,7 +225,7 @@

Comunicar uma vulnerabilidade

diff --git a/docs/releases/index.html b/docs/releases/index.html index f81c084d..1253b22a 100644 --- a/docs/releases/index.html +++ b/docs/releases/index.html @@ -61,7 +61,21 @@

What's new

-

0.15.1

Latest
+

0.15.2

Latest
+
    +
  • Repair managed-pack activity on upgraded databases. Flowlight now verifies the physical + activity key during startup and atomically repairs stale schemas, preserving existing rows before any + blocked connection is recorded.
  • +
  • Keep pack decisions distinct and exactly once. Exact and suffix matches retain separate + provenance, repeated decisions aggregate correctly, and retried extension deliveries do not double count.
  • +
  • Keep editor feedback truthful under partial failures. Incomplete app scans still accept + explicit bundle identifiers, previews expire when traffic or policies change, and exceptions appear only after their + database write succeeds with a concrete error when it does not.
  • +
+
+ +
+

0.15.1

  • Keep managed-pack changes safely in review. Opening or cancelling an off pack no longer activates it, and configuration changes persist only after an explicit Save succeeds.
  • @@ -1245,7 +1259,7 @@

    What's new

diff --git a/docs/threat-model/index.html b/docs/threat-model/index.html index 1d879978..c78b405f 100644 --- a/docs/threat-model/index.html +++ b/docs/threat-model/index.html @@ -224,7 +224,7 @@

Reporting a vulnerability

diff --git a/docs/zh-Hans/about/index.html b/docs/zh-Hans/about/index.html index 60f968de..98a97cef 100644 --- a/docs/zh-Hans/about/index.html +++ b/docs/zh-Hans/about/index.html @@ -152,7 +152,7 @@

致谢

diff --git a/docs/zh-Hans/docs/index.html b/docs/zh-Hans/docs/index.html index 684152b1..37a56a90 100644 --- a/docs/zh-Hans/docs/index.html +++ b/docs/zh-Hans/docs/index.html @@ -76,7 +76,7 @@

文档

使用 Flowlight

-

从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.15.1,macOS 15 或更高版本。

+

从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.15.2,macOS 15 或更高版本。

diff --git a/docs/zh-Hans/index.html b/docs/zh-Hans/index.html index fd06a1b1..38f7f600 100644 --- a/docs/zh-Hans/index.html +++ b/docs/zh-Hans/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

看清应用的网络活动。

brew install --cask xinbetween/tap/flowlight

-

v0.15.1macOS 15+通用架构GPL-3.0无遥测

+

v0.15.2macOS 15+通用架构GPL-3.0无遥测

@@ -568,7 +568,7 @@

知道什么离开了你的 Mac。

diff --git a/docs/zh-Hans/privacy/index.html b/docs/zh-Hans/privacy/index.html index 090e8ea1..4af5385f 100644 --- a/docs/zh-Hans/privacy/index.html +++ b/docs/zh-Hans/privacy/index.html @@ -186,7 +186,7 @@

联系方式

diff --git a/docs/zh-Hans/threat-model/index.html b/docs/zh-Hans/threat-model/index.html index 679dd69f..0228b8fa 100644 --- a/docs/zh-Hans/threat-model/index.html +++ b/docs/zh-Hans/threat-model/index.html @@ -212,7 +212,7 @@

报告漏洞

diff --git a/docs/zh-Hant/about/index.html b/docs/zh-Hant/about/index.html index da99e59f..6d9c475f 100644 --- a/docs/zh-Hant/about/index.html +++ b/docs/zh-Hant/about/index.html @@ -152,7 +152,7 @@

致謝

diff --git a/docs/zh-Hant/docs/index.html b/docs/zh-Hant/docs/index.html index b207822a..acd28ef3 100644 --- a/docs/zh-Hant/docs/index.html +++ b/docs/zh-Hant/docs/index.html @@ -76,7 +76,7 @@

說明文件

使用 Flowlight

-

從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.15.1,macOS 15 或以上版本。

+

從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.15.2,macOS 15 或以上版本。

diff --git a/docs/zh-Hant/index.html b/docs/zh-Hant/index.html index dee17a05..3781e9a3 100644 --- a/docs/zh-Hant/index.html +++ b/docs/zh-Hant/index.html @@ -48,7 +48,7 @@ - + @@ -90,7 +90,7 @@

看見每個 App 的網路活動。
brew install --cask xinbetween/tap/flowlight

-

v0.15.1macOS 15+通用架構GPL-3.0無遙測

+

v0.15.2macOS 15+通用架構GPL-3.0無遙測

@@ -569,7 +569,7 @@

知道有什麼離開了你的 Mac。

diff --git a/docs/zh-Hant/privacy/index.html b/docs/zh-Hant/privacy/index.html index 4d878aab..9cc093f4 100644 --- a/docs/zh-Hant/privacy/index.html +++ b/docs/zh-Hant/privacy/index.html @@ -186,7 +186,7 @@

聯絡

diff --git a/docs/zh-Hant/threat-model/index.html b/docs/zh-Hant/threat-model/index.html index 7865721a..d2e25726 100644 --- a/docs/zh-Hant/threat-model/index.html +++ b/docs/zh-Hant/threat-model/index.html @@ -213,7 +213,7 @@

回報漏洞

diff --git a/project.yml b/project.yml index 0c724f03..e68a22d0 100644 --- a/project.yml +++ b/project.yml @@ -11,8 +11,8 @@ settings: DEVELOPMENT_TEAM: "" CODE_SIGN_STYLE: Automatic ENABLE_HARDENED_RUNTIME: YES - MARKETING_VERSION: "0.15.1" - CURRENT_PROJECT_VERSION: "37" + MARKETING_VERSION: "0.15.2" + CURRENT_PROJECT_VERSION: "38" targets: Flowlight: type: application diff --git a/site/pages/releases.html b/site/pages/releases.html index 6cc47e07..60db4f41 100644 --- a/site/pages/releases.html +++ b/site/pages/releases.html @@ -13,7 +13,21 @@

What's new

-

0.15.1

Latest
+

0.15.2

Latest
+
    +
  • Repair managed-pack activity on upgraded databases. Flowlight now verifies the physical + activity key during startup and atomically repairs stale schemas, preserving existing rows before any + blocked connection is recorded.
  • +
  • Keep pack decisions distinct and exactly once. Exact and suffix matches retain separate + provenance, repeated decisions aggregate correctly, and retried extension deliveries do not double count.
  • +
  • Keep editor feedback truthful under partial failures. Incomplete app scans still accept + explicit bundle identifiers, previews expire when traffic or policies change, and exceptions appear only after their + database write succeeds with a concrete error when it does not.
  • +
+
+ +
+

0.15.1

  • Keep managed-pack changes safely in review. Opening or cancelling an off pack no longer activates it, and configuration changes persist only after an explicit Save succeeds.