From 0dd0dc7ee9b26319edaef8e1ea8900ad96b42b3b Mon Sep 17 00:00:00 2001 From: blessdyb Date: Sun, 27 Sep 2026 00:35:56 -0700 Subject: [PATCH 1/2] Verify an update before installing it, not after MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two checks in the update path answered "yes" when they meant "I don't know". The checksum was compared as `expected == nil || expected == actual`. A release with no `SHA256SUMS.txt`, a checksums file with no line for the disk image, or one that didn't parse all produced a nil expectation, and a nil expectation passed — so every way of failing to learn the checksum was treated as having learned it was right. It now requires the file, a 200 for it, and a line for this image, and refuses the download otherwise. Every release the workflow publishes carries one; a release that doesn't is one to refuse. The installer checked the downloaded app's version and bundle identifier. Both are strings inside the disk image, and nothing signs them. It then removed the quarantine attribute — the one thing that would have made macOS check the signature when the app was first launched. So the check that mattered was never made, and the check macOS would have made was deleted. `CodeSignatureCheck` now validates the staged bundle against Apple's anchor with nested code and strict validation, and against the Team ID of the running app rather than one written down here: an update has to be signed by whoever signed the copy asking for it. That needs no constant that can go stale, and on an ad-hoc local build — no team to compare against — it refuses instead of pretending it verified something. `FLSkipUpdateSignatureCheck` lets those local builds exercise the path; it is read from the running app's own defaults, so nothing inside a downloaded image can set it. Co-Authored-By: Claude Opus 5 (1M context) --- Flowlight/App/CodeSignatureCheck.swift | 81 ++++++++++++++++++++ Flowlight/App/UpdateChecker.swift | 23 ++++-- Flowlight/App/UpdateInstaller.swift | 24 ++++++ Flowlight/Ask/RemoteAsk.swift | 26 +++++++ Flowlight/Inspection/HTTPStream.swift | 28 ++++++- FlowlightTests/UpdateVerificationTests.swift | 47 ++++++++++++ 6 files changed, 219 insertions(+), 10 deletions(-) create mode 100644 Flowlight/App/CodeSignatureCheck.swift create mode 100644 FlowlightTests/UpdateVerificationTests.swift diff --git a/Flowlight/App/CodeSignatureCheck.swift b/Flowlight/App/CodeSignatureCheck.swift new file mode 100644 index 0000000..5d0518a --- /dev/null +++ b/Flowlight/App/CodeSignatureCheck.swift @@ -0,0 +1,81 @@ +import Foundation +import Security + +/// Whether a bundle about to replace this one was signed by whoever signed this one. +/// +/// The update path checked that the downloaded app called itself Flowlight and carried the expected version — +/// both of which are strings inside the bundle, and neither of which anything signs. A disk image that got +/// past the checksum would have been installed on the strength of its own `Info.plist`, and the installer then +/// removed the quarantine flag, which is the attribute that would have made macOS check the signature on first +/// launch. So the one check that mattered was being skipped and the one macOS would have done was being +/// deleted. +/// +/// The team is read from the running app rather than written down here. "Signed by the same team as the copy +/// asking for the update" is the property that actually matters, it needs no constant to fall out of date, and +/// on an unsigned local build — where `teamIdentifier` is nil — it declines to pretend it verified anything. +enum CodeSignatureCheck { + enum Failure: LocalizedError, Equatable { + case unreadable(OSStatus) + case notSigned + case wrongTeam(found: String?, expected: String) + case invalid(OSStatus) + case selfUnknown + + var errorDescription: String? { + switch self { + case .unreadable(let status): + return "The downloaded app's signature couldn't be read (OSStatus \(status))." + case .notSigned: + return "The downloaded app isn't signed." + case .wrongTeam(let found, let expected): + return "The downloaded app is signed by team \(found ?? "none"), not \(expected)." + case .invalid(let status): + return "The downloaded app's signature didn't verify (OSStatus \(status))." + case .selfUnknown: + return "This copy of Flowlight isn't signed with a Developer ID, so it can't tell whether an update is." + } + } + } + + /// The Team ID of the running process, or nil when it has no Developer ID — an ad-hoc local build. + static func runningTeamIdentifier() -> String? { + var code: SecCode? + guard SecCodeCopySelf([], &code) == errSecSuccess, let code else { return nil } + var staticCode: SecStaticCode? + guard SecCodeCopyStaticCode(code, [], &staticCode) == errSecSuccess, let staticCode else { return nil } + return teamIdentifier(of: staticCode) + } + + private static func teamIdentifier(of code: SecStaticCode) -> String? { + var info: CFDictionary? + guard SecCodeCopySigningInformation(code, SecCSFlags(rawValue: kSecCSSigningInformation), &info) == errSecSuccess, + let dictionary = info as? [String: Any] else { return nil } + return dictionary[kSecCodeInfoTeamIdentifier as String] as? String + } + + /// Throws unless `url` is a valid signature from `expectedTeam`, checked with the same strictness Gatekeeper + /// uses: the whole bundle, nested code included, against Apple's anchor. + static func verify(_ url: URL, expectedTeam: String) throws { + var staticCode: SecStaticCode? + let created = SecStaticCodeCreateWithPath(url as CFURL, [], &staticCode) + guard created == errSecSuccess, let staticCode else { throw Failure.unreadable(created) } + + // Signed by Apple's Developer ID anchor, by this team, and every nested binary along with it. The + // requirement is what makes this more than "has a signature": an attacker's own valid signature fails. + let requirement = "anchor apple generic and certificate leaf[subject.OU] = \"\(expectedTeam)\"" + var securityRequirement: SecRequirement? + guard SecRequirementCreateWithString(requirement as CFString, [], &securityRequirement) == errSecSuccess, + let securityRequirement else { throw Failure.unreadable(errSecParam) } + + let flags = SecCSFlags(rawValue: kSecCSCheckAllArchitectures | kSecCSCheckNestedCode | kSecCSStrictValidate) + let status = SecStaticCodeCheckValidity(staticCode, flags, securityRequirement) + guard status == errSecSuccess else { + // Say which of the two it was, because "signed by someone else" and "signature damaged" mean very + // different things to whoever reads the error. + let found = teamIdentifier(of: staticCode) + if let found, found != expectedTeam { throw Failure.wrongTeam(found: found, expected: expectedTeam) } + if found == nil { throw Failure.notSigned } + throw Failure.invalid(status) + } + } +} diff --git a/Flowlight/App/UpdateChecker.swift b/Flowlight/App/UpdateChecker.swift index b25951f..55983e1 100644 --- a/Flowlight/App/UpdateChecker.swift +++ b/Flowlight/App/UpdateChecker.swift @@ -31,7 +31,7 @@ struct AppRelease: Equatable, Sendable { } enum UpdateError: LocalizedError, Equatable { - case noRelease, badResponse(Int), checksumMismatch, noDownload + case noRelease, badResponse(Int), checksumMismatch, noDownload, noChecksum var errorDescription: String? { switch self { @@ -39,6 +39,7 @@ enum UpdateError: LocalizedError, Equatable { case .badResponse(let code): return code == 403 ? "GitHub is rate-limiting update checks. Try again later." : "GitHub returned HTTP \(code)." case .checksumMismatch: return "The download didn't match its published checksum, so it wasn't opened." case .noDownload: return "This release has no disk image to download." + case .noChecksum: return "This release publishes no checksum for its disk image, so the download wasn't opened." } } } @@ -205,12 +206,20 @@ final class UpdateChecker: ObservableObject { let (temp, response) = try await session.download(from: dmgURL, delegate: nil) let code = (response as? HTTPURLResponse)?.statusCode ?? 0 guard code == 200 else { throw UpdateError.badResponse(code) } - if let sumsURL = release.checksumsURL { - let (sums, _) = try await session.data(from: sumsURL) - let expected = VersionCompare.checksum(for: dmgURL.lastPathComponent, in: String(decoding: sums, as: UTF8.self)) - let actual = SHA256.hash(data: try Data(contentsOf: temp)).map { String(format: "%02x", $0) }.joined() - guard expected == nil || expected == actual else { throw UpdateError.checksumMismatch } - } + // Fail closed. This used to accept the download when the release had no SHA256SUMS.txt, or had + // one with no line for this disk image, or one that didn't parse — every way of *not knowing* + // the checksum was treated as knowing it was right, which is the one outcome verification must + // never produce. Every release the workflow publishes carries the file; a release that doesn't + // is one to refuse rather than to trust. + guard let sumsURL = release.checksumsURL else { throw UpdateError.noChecksum } + let (sums, sumsResponse) = try await session.data(from: sumsURL) + let sumsCode = (sumsResponse as? HTTPURLResponse)?.statusCode ?? 0 + guard sumsCode == 200 else { throw UpdateError.badResponse(sumsCode) } + guard let expected = VersionCompare.checksum(for: dmgURL.lastPathComponent, + in: String(decoding: sums, as: UTF8.self)) + else { throw UpdateError.noChecksum } + let actual = SHA256.hash(data: try Data(contentsOf: temp)).map { String(format: "%02x", $0) }.joined() + guard expected.caseInsensitiveCompare(actual) == .orderedSame else { throw UpdateError.checksumMismatch } let downloads = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0] let destination = downloads.appendingPathComponent("Flowlight-\(release.version).dmg") try? FileManager.default.removeItem(at: destination) diff --git a/Flowlight/App/UpdateInstaller.swift b/Flowlight/App/UpdateInstaller.swift index 05bb831..a5157c2 100644 --- a/Flowlight/App/UpdateInstaller.swift +++ b/Flowlight/App/UpdateInstaller.swift @@ -31,6 +31,25 @@ enum UpdateInstaller { return bundleURL } + /// Whether an update may be installed: signed by this app's own team, or — for a build that has no team, + /// which is an ad-hoc local one — refused, because a copy that cannot prove who signed it cannot judge + /// anyone else's signature either. + /// + /// `FLSkipUpdateSignatureCheck` exists for the same local builds, which are unsigned and could otherwise + /// never test the update path at all. It is read from the defaults of the *running* app, so it cannot be + /// set by anything inside a downloaded disk image. + static func verifySignature(of app: URL) throws { + if UserDefaults.standard.bool(forKey: "FLSkipUpdateSignatureCheck") { return } + guard let team = CodeSignatureCheck.runningTeamIdentifier() else { + throw InstallError.wrongApp(CodeSignatureCheck.Failure.selfUnknown.errorDescription ?? "unsigned") + } + do { + try CodeSignatureCheck.verify(app, expectedTeam: team) + } catch { + throw InstallError.wrongApp(error.localizedDescription) + } + } + /// Mounts the disk image, copies Flowlight.app to a staging folder and checks it's the expected version. static func stage(dmg: URL, expectedVersion: String, bundleID: String?) throws -> URL { let mount = FileManager.default.temporaryDirectory.appendingPathComponent("flowlight-update-\(UUID().uuidString)") @@ -49,6 +68,11 @@ enum UpdateInstaller { if let bundleID, bundle.bundleIdentifier != bundleID { throw InstallError.wrongApp("bundle \(bundle.bundleIdentifier ?? "?")") } + // The version and the bundle id above are strings inside the disk image, which nothing signs. This is + // the check that can't be forged: the update has to be signed by the same team as the copy asking for + // it, with a valid Developer ID signature over every nested binary. It runs before anything is staged, + // and the quarantine flag is only dropped later because this passed. + try verifySignature(of: source) let staging = FileManager.default.temporaryDirectory.appendingPathComponent("flowlight-staged-\(UUID().uuidString)") try FileManager.default.createDirectory(at: staging, withIntermediateDirectories: true) diff --git a/Flowlight/Ask/RemoteAsk.swift b/Flowlight/Ask/RemoteAsk.swift index 73a3396..f3f0010 100644 --- a/Flowlight/Ask/RemoteAsk.swift +++ b/Flowlight/Ask/RemoteAsk.swift @@ -38,11 +38,37 @@ struct RemoteProvider: AskProviding { } } + /// HTTPS anywhere, or plain HTTP only where the traffic cannot leave the machine or the local network. + static func isSafeEndpoint(_ url: URL) -> Bool { + switch url.scheme?.lowercased() { + case "https": return true + case "http": break + default: return false + } + guard let host = url.host?.lowercased() else { return false } + if host == "localhost" || host == "::1" || host.hasSuffix(".local") { return true } + if host == "127.0.0.1" || host.hasPrefix("127.") { return true } + // The private ranges, for a model served from another machine on the same network. + if host.hasPrefix("10.") || host.hasPrefix("192.168.") { return true } + if host.hasPrefix("172.") { + let second = host.split(separator: ".").dropFirst().first.flatMap { Int($0) } ?? -1 + return (16...31).contains(second) + } + return false + } + func answer(_ request: AskRequest, run: @escaping @Sendable (AskCall) async -> String, sending: @escaping @Sendable (String) -> Void) async throws -> String { guard let url = URL(string: endpoint), !endpoint.isEmpty else { throw Failure.notConfigured(L("No endpoint is set for %@.", kind.title)) } + // The request carries an API key and a question about this Mac's own traffic. Over http:// both are + // readable by anything on the path — including, with some irony, Flowlight. A loopback or private + // address is the exception worth keeping: that is how someone points this at a model running on their + // own machine or LAN, where there is no network to eavesdrop on. + guard Self.isSafeEndpoint(url) else { + throw Failure.notConfigured(L("%@ is not an https:// address. A key and your question would cross the network in the clear, so Flowlight won't send them.", endpoint)) + } guard !kind.needsKey || !apiKey.isEmpty else { throw Failure.notConfigured(L("%@ needs an API key. Add one in Settings — it goes to your login Keychain.", kind.title)) } diff --git a/Flowlight/Inspection/HTTPStream.swift b/Flowlight/Inspection/HTTPStream.swift index d01387c..2d93c3d 100644 --- a/Flowlight/Inspection/HTTPStream.swift +++ b/Flowlight/Inspection/HTTPStream.swift @@ -247,12 +247,34 @@ enum HeaderRedaction { "x-auth-token", "x-amz-security-token", "x-csrf-token", "x-xsrf-token", "openai-organization-key", ] + /// Words that make a header a credential whatever it is called. The named list above can only know the + /// headers someone thought of: `X-Access-Key`, `X-Client-Credential` and every vendor's own spelling went + /// straight into the database, under a promise that says API keys are redacted. Matching on the word + /// rather than the whole name is what closes that, at the cost of occasionally hiding a value that wasn't + /// secret — the right direction to err in for something written to disk. + static let secretWords = ["token", "secret", "api-key", "apikey", "key", "auth", "credential", "password", + "passwd", "session", "signature", "sig", "nonce", "bearer"] + + /// Headers whose name contains one of those words but which carry no secret — without these, ordinary + /// request metadata would be redacted and the recorded exchange would be harder to read for no gain. + static let notSecret: Set = [ + "keep-alive", "x-request-id", "x-correlation-id", "x-session-duration", "content-signature-algorithm", + ] + + static func isSecret(_ name: String) -> Bool { + let lower = name.lowercased() + if notSecret.contains(lower) { return false } + if secretNames.contains(lower) { return true } + // Word-ish boundaries, so `x-api-key` and `x_auth_token` match while `monkey` and `authority` don't. + let parts = lower.split(whereSeparator: { $0 == "-" || $0 == "_" || $0 == "." }).map(String.init) + if parts.contains(where: { secretWords.contains($0) }) { return true } + return secretWords.contains { $0.contains("-") && lower.contains($0) } + } + static func redact(_ headers: [HTTPHeader]) -> [HTTPHeader] { headers.map { header in let name = header.name.lowercased() - guard secretNames.contains(name) || name.hasSuffix("-token") || name.hasSuffix("-secret") || name.contains("api-key") else { - return header - } + guard isSecret(name) else { return header } // Keep the scheme ("Bearer") so the kind of credential is still visible. let scheme = header.value.split(separator: " ").first.map(String.init) let keepScheme = name.hasSuffix("authorization") && header.value.contains(" ") ? scheme.map { $0 + " " } ?? "" : "" diff --git a/FlowlightTests/UpdateVerificationTests.swift b/FlowlightTests/UpdateVerificationTests.swift new file mode 100644 index 0000000..9d9dc59 --- /dev/null +++ b/FlowlightTests/UpdateVerificationTests.swift @@ -0,0 +1,47 @@ +import XCTest +@testable import Flowlight + +final class UpdateVerificationTests: XCTestCase { + private let sums = """ + a3f1c2d4e5b6a7980123456789abcdef0123456789abcdef0123456789abcdef Flowlight.dmg + 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff Flowlight-0.8.2.pkg + """ + + func testAChecksumIsFoundByFileName() { + XCTAssertEqual(VersionCompare.checksum(for: "Flowlight.dmg", in: sums), + "a3f1c2d4e5b6a7980123456789abcdef0123456789abcdef0123456789abcdef") + } + + /// The case that made the old check useless: a checksum file that says nothing about this file. It used to + /// read as "no expectation, so anything matches"; the download path now treats nil as a refusal. + func testAMissingEntryIsNilRatherThanAMatch() { + XCTAssertNil(VersionCompare.checksum(for: "Flowlight.dmg", in: "not a checksum file at all")) + XCTAssertNil(VersionCompare.checksum(for: "Something-Else.dmg", in: sums)) + } + + /// An update signed by somebody else is the attack this exists to stop, so the two failures have to be + /// distinguishable — "signed by another team" is a different event from "signature damaged". + func testTheTeamMismatchIsReportedAsItself() throws { + let failure = CodeSignatureCheck.Failure.wrongTeam(found: "ATTACKER99", expected: "ABCDE12345") + XCTAssertEqual(failure, .wrongTeam(found: "ATTACKER99", expected: "ABCDE12345")) + XCTAssertNotEqual(failure, .notSigned) + let description = try XCTUnwrap(failure.errorDescription) + XCTAssertTrue(description.contains("ATTACKER99")) + XCTAssertTrue(description.contains("ABCDE12345")) + } + + /// Verification has to fail on a bundle that isn't signed at all, rather than passing it for lack of a + /// signature to disagree with. `/bin` is a real path that is not a signed app bundle. + func testAnUnsignedPathDoesNotVerify() { + XCTAssertThrowsError(try CodeSignatureCheck.verify(URL(fileURLWithPath: "/bin"), expectedTeam: "ABCDE12345")) + } + + /// The running app is what decides which team an update must carry. On a signed build this is the Team ID; + /// on an ad-hoc local build it is nil, and the installer refuses rather than guessing. + func testTheExpectedTeamComesFromTheRunningApp() { + // Either answer is correct depending on how the tests were built; what matters is that asking is safe + // and that nil is an answer the caller has to handle rather than a crash. + let team = CodeSignatureCheck.runningTeamIdentifier() + if let team { XCTAssertFalse(team.isEmpty) } + } +} From e2faa4fec5ce5054e5c46dd1fa01e74f8b9b5738 Mon Sep 17 00:00:00 2001 From: blessdyb Date: Sun, 27 Sep 2026 00:36:24 -0700 Subject: [PATCH 2/2] Flowlight 0.8.2 Version, notes and rebuilt site, on the release branch until the tag has published. Co-Authored-By: Claude Opus 5 (1M context) --- 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/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/fr/about/index.html | 2 +- docs/fr/docs/index.html | 4 ++-- docs/fr/index.html | 6 +++--- docs/fr/privacy/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/ja/about/index.html | 2 +- docs/ja/docs/index.html | 4 ++-- docs/ja/index.html | 6 +++--- docs/ja/privacy/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/llms-full.txt | 32 +++++++++++++++++++++++++++++--- docs/llms.txt | 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/releases/index.html | 27 +++++++++++++++++++++++++-- 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-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 +- project.yml | 2 +- site/pages/releases.html | 25 ++++++++++++++++++++++++- 46 files changed, 151 insertions(+), 79 deletions(-) diff --git a/docs/404.html b/docs/404.html index 494739f..0c439bd 100644 --- a/docs/404.html +++ b/docs/404.html @@ -81,7 +81,7 @@

That page isn't here

Try the home page,

diff --git a/docs/about/index.html b/docs/about/index.html index 5ad8649..d8f93f8 100644 --- a/docs/about/index.html +++ b/docs/about/index.html @@ -139,7 +139,7 @@

Thanks

diff --git a/docs/de/about/index.html b/docs/de/about/index.html index b79a22b..c80f599 100644 --- a/docs/de/about/index.html +++ b/docs/de/about/index.html @@ -138,7 +138,7 @@

Dank

diff --git a/docs/de/docs/index.html b/docs/de/docs/index.html index f54e210..353820e 100644 --- a/docs/de/docs/index.html +++ b/docs/de/docs/index.html @@ -62,7 +62,7 @@

Dokumentation

Flowlight benutzen

-

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

+

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

diff --git a/docs/de/index.html b/docs/de/index.html index fd0e08a..5db4dbc 100644 --- a/docs/de/index.html +++ b/docs/de/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

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

-

v0.8.1macOS 15+UniversalGPL-3.0Keine Telemetrie

+

v0.8.2macOS 15+UniversalGPL-3.0Keine Telemetrie

@@ -613,7 +613,7 @@

Wisse, was deinen Mac verlässt.

diff --git a/docs/de/privacy/index.html b/docs/de/privacy/index.html index d15556d..dd60cb4 100644 --- a/docs/de/privacy/index.html +++ b/docs/de/privacy/index.html @@ -168,7 +168,7 @@

Kontakt

diff --git a/docs/docs/index.html b/docs/docs/index.html index 5512051..4a98779 100644 --- a/docs/docs/index.html +++ b/docs/docs/index.html @@ -62,7 +62,7 @@

Documentation

Using Flowlight

-

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

+

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

diff --git a/docs/es/about/index.html b/docs/es/about/index.html index 61586b9..82ac38e 100644 --- a/docs/es/about/index.html +++ b/docs/es/about/index.html @@ -138,7 +138,7 @@

Agradecimientos

diff --git a/docs/es/docs/index.html b/docs/es/docs/index.html index 623e830..cbbc663 100644 --- a/docs/es/docs/index.html +++ b/docs/es/docs/index.html @@ -62,7 +62,7 @@

Documentación

Usar Flowlight

-

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

+

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

diff --git a/docs/es/index.html b/docs/es/index.html index b0f9026..cd6d881 100644 --- a/docs/es/index.html +++ b/docs/es/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

Observa la actividad de red de tus apps.

brew install --cask xinbetween/tap/flowlight

-

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

+

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

@@ -613,7 +613,7 @@

Ten claro qué sale de tu Mac.

diff --git a/docs/es/privacy/index.html b/docs/es/privacy/index.html index 94bfba0..7276c6e 100644 --- a/docs/es/privacy/index.html +++ b/docs/es/privacy/index.html @@ -168,7 +168,7 @@

Contacto

diff --git a/docs/fr/about/index.html b/docs/fr/about/index.html index ae4e0b5..28d1cf1 100644 --- a/docs/fr/about/index.html +++ b/docs/fr/about/index.html @@ -138,7 +138,7 @@

Remerciements

diff --git a/docs/fr/docs/index.html b/docs/fr/docs/index.html index 09d6d62..9a4d3ca 100644 --- a/docs/fr/docs/index.html +++ b/docs/fr/docs/index.html @@ -62,7 +62,7 @@

Documentation

Utiliser Flowlight

-

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

+

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

diff --git a/docs/fr/index.html b/docs/fr/index.html index 09190d6..d4a0d67 100644 --- a/docs/fr/index.html +++ b/docs/fr/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

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

brew install --cask xinbetween/tap/flowlight

-

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

+

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

@@ -613,7 +613,7 @@

Sachez ce qui quitte votre Mac.

diff --git a/docs/fr/privacy/index.html b/docs/fr/privacy/index.html index 60fc694..8618645 100644 --- a/docs/fr/privacy/index.html +++ b/docs/fr/privacy/index.html @@ -168,7 +168,7 @@

Contact

diff --git a/docs/index.html b/docs/index.html index 95a3683..2f4e02f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

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

-

v0.8.1macOS 15+UniversalGPL-3.0No telemetry

+

v0.8.2macOS 15+UniversalGPL-3.0No telemetry

@@ -614,7 +614,7 @@

Know what leaves your Mac.

diff --git a/docs/it/about/index.html b/docs/it/about/index.html index f519c3c..47a856b 100644 --- a/docs/it/about/index.html +++ b/docs/it/about/index.html @@ -138,7 +138,7 @@

Ringraziamenti

diff --git a/docs/it/docs/index.html b/docs/it/docs/index.html index f890837..21b7666 100644 --- a/docs/it/docs/index.html +++ b/docs/it/docs/index.html @@ -62,7 +62,7 @@

Documentazione

Usare Flowlight

-

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

+

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

diff --git a/docs/it/index.html b/docs/it/index.html index 4e3dc20..2e4a2b8 100644 --- a/docs/it/index.html +++ b/docs/it/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

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

brew install --cask xinbetween/tap/flowlight

-

v0.8.1macOS 15+UniversaleGPL-3.0Nessuna telemetria

+

v0.8.2macOS 15+UniversaleGPL-3.0Nessuna telemetria

@@ -613,7 +613,7 @@

Sappi che cosa esce dal tuo Mac.

diff --git a/docs/it/privacy/index.html b/docs/it/privacy/index.html index 2ec7e71..f777b6d 100644 --- a/docs/it/privacy/index.html +++ b/docs/it/privacy/index.html @@ -168,7 +168,7 @@

Contatti

diff --git a/docs/ja/about/index.html b/docs/ja/about/index.html index 1bf4d93..4128921 100644 --- a/docs/ja/about/index.html +++ b/docs/ja/about/index.html @@ -139,7 +139,7 @@

謝辞

diff --git a/docs/ja/docs/index.html b/docs/ja/docs/index.html index 7571189..66e4ee3 100644 --- a/docs/ja/docs/index.html +++ b/docs/ja/docs/index.html @@ -62,7 +62,7 @@

ドキュメント

Flowlight の使い方

-

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

+

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

diff --git a/docs/ja/index.html b/docs/ja/index.html index 08c9e59..0062be1 100644 --- a/docs/ja/index.html +++ b/docs/ja/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

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

-

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

+

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

@@ -612,7 +612,7 @@

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

diff --git a/docs/ja/privacy/index.html b/docs/ja/privacy/index.html index 840041e..eefdc42 100644 --- a/docs/ja/privacy/index.html +++ b/docs/ja/privacy/index.html @@ -167,7 +167,7 @@

連絡先

diff --git a/docs/ko/about/index.html b/docs/ko/about/index.html index 1e2953d..a9df8e2 100644 --- a/docs/ko/about/index.html +++ b/docs/ko/about/index.html @@ -139,7 +139,7 @@

감사

diff --git a/docs/ko/docs/index.html b/docs/ko/docs/index.html index 827afd4..136324e 100644 --- a/docs/ko/docs/index.html +++ b/docs/ko/docs/index.html @@ -62,7 +62,7 @@

문서

Flowlight 사용하기

-

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

+

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

diff --git a/docs/ko/index.html b/docs/ko/index.html index 2e49306..bed74bf 100644 --- a/docs/ko/index.html +++ b/docs/ko/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

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

brew install --cask xinbetween/tap/flowlight

-

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

+

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

@@ -613,7 +613,7 @@

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

diff --git a/docs/ko/privacy/index.html b/docs/ko/privacy/index.html index 44099ad..915aab6 100644 --- a/docs/ko/privacy/index.html +++ b/docs/ko/privacy/index.html @@ -170,7 +170,7 @@

문의

diff --git a/docs/llms-full.txt b/docs/llms-full.txt index ff2d7d9..f854274 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.8.1, macOS 15 or later. + Everything from the first launch to tuning the agent rules. Flowlight 0.8.2, macOS 15 or later. On this page @@ -698,7 +698,7 @@ Understand your AI agents. brew install --cask xinbetween/tap/flowlightCopy - v0.8.1macOS 15+UniversalGPL-3.0No telemetry + v0.8.2macOS 15+UniversalGPL-3.0No telemetry connectionslive @@ -1244,8 +1244,34 @@ Releases Downloads, checksums and full notes for each version are on GitHub Releases. + 0.8.2 +September 27, 2026Latest + + An update is verified before it is installed. The checksum comparison treated + "no checksum published", "no line for this disk image" and "the file didn't parse" as a match — + every way of not knowing was read as knowing it was right. It now requires a published checksum and + refuses the download without one. + + And it has to be signed by us. The installer checked the downloaded app's + version and bundle identifier — strings inside the disk image that nothing signs — and then removed + the quarantine flag, which is what would have made macOS check the signature on first launch. + Updates are now validated against Apple's anchor and the Team ID of the running copy: an update has + to come from whoever signed the Flowlight asking for it. + + A model endpoint can't take your key over plain HTTP. A compatible hosted + endpoint sent an API key and a question about your own traffic to whatever address was typed. + https:// is now required, except for loopback and private addresses, where the model is + running on your own machine or network. + + Credential headers are recognised by their words, not by a list. Redaction knew + the common names, so X-Access-Key or a vendor's own spelling was written to disk under + a promise that says API keys are never stored. Anything whose name carries key, token, secret, + auth, credential, session or signature is redacted now. + + Downloads and checksums → + 0.8.1 -September 26, 2026Latest +September 26, 2026 Flowlight stops working when you are not looking at it. It was rebuilding the live chart and the per-app list every second whenever a window existed — including when that window was diff --git a/docs/llms.txt b/docs/llms.txt index 20477c5..cd01d65 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.8.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.8.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/privacy/index.html b/docs/privacy/index.html index 5e5e485..1224dfe 100644 --- a/docs/privacy/index.html +++ b/docs/privacy/index.html @@ -169,7 +169,7 @@

Contact

diff --git a/docs/pt-PT/about/index.html b/docs/pt-PT/about/index.html index 29179f4..9025e5d 100644 --- a/docs/pt-PT/about/index.html +++ b/docs/pt-PT/about/index.html @@ -138,7 +138,7 @@

Agradecimentos

diff --git a/docs/pt-PT/docs/index.html b/docs/pt-PT/docs/index.html index 8c67b06..09c41f6 100644 --- a/docs/pt-PT/docs/index.html +++ b/docs/pt-PT/docs/index.html @@ -62,7 +62,7 @@

Documentação

Usar o Flowlight

-

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

+

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

diff --git a/docs/pt-PT/index.html b/docs/pt-PT/index.html index ade2211..5973523 100644 --- a/docs/pt-PT/index.html +++ b/docs/pt-PT/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

Veja a atividade de rede das suas apps.

brew install --cask xinbetween/tap/flowlight

-

v0.8.1macOS 15+UniversalGPL-3.0Sem telemetria

+

v0.8.2macOS 15+UniversalGPL-3.0Sem telemetria

@@ -613,7 +613,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 d5066ce..2dc6d65 100644 --- a/docs/pt-PT/privacy/index.html +++ b/docs/pt-PT/privacy/index.html @@ -168,7 +168,7 @@

Contacto

diff --git a/docs/releases/index.html b/docs/releases/index.html index aff1e91..e6406f5 100644 --- a/docs/releases/index.html +++ b/docs/releases/index.html @@ -56,7 +56,30 @@

What's new

-

0.8.1

Latest
+

0.8.2

Latest
+
    +
  • An update is verified before it is installed. The checksum comparison treated + "no checksum published", "no line for this disk image" and "the file didn't parse" as a match — + every way of not knowing was read as knowing it was right. It now requires a published checksum and + refuses the download without one.
  • +
  • And it has to be signed by us. The installer checked the downloaded app's + version and bundle identifier — strings inside the disk image that nothing signs — and then removed + the quarantine flag, which is what would have made macOS check the signature on first launch. + Updates are now validated against Apple's anchor and the Team ID of the running copy: an update has + to come from whoever signed the Flowlight asking for it.
  • +
  • A model endpoint can't take your key over plain HTTP. A compatible hosted + endpoint sent an API key and a question about your own traffic to whatever address was typed. + https:// is now required, except for loopback and private addresses, where the model is + running on your own machine or network.
  • +
  • Credential headers are recognised by their words, not by a list. Redaction knew + the common names, so X-Access-Key or a vendor's own spelling was written to disk under + a promise that says API keys are never stored. Anything whose name carries key, token, secret, + auth, credential, session or signature is redacted now.
  • +
+

Downloads and checksums →

+
+
+

0.8.1

  • Flowlight stops working when you are not looking at it. It was rebuilding the live chart and the per-app list every second whenever a window existed — including when that window was @@ -734,7 +757,7 @@

    What's new

diff --git a/docs/zh-Hans/about/index.html b/docs/zh-Hans/about/index.html index 3a8543b..fee8244 100644 --- a/docs/zh-Hans/about/index.html +++ b/docs/zh-Hans/about/index.html @@ -138,7 +138,7 @@

致谢

diff --git a/docs/zh-Hans/docs/index.html b/docs/zh-Hans/docs/index.html index 60c4d5e..99f743d 100644 --- a/docs/zh-Hans/docs/index.html +++ b/docs/zh-Hans/docs/index.html @@ -62,7 +62,7 @@

文档

使用 Flowlight

-

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

+

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

diff --git a/docs/zh-Hans/index.html b/docs/zh-Hans/index.html index bf5196e..1e2404c 100644 --- a/docs/zh-Hans/index.html +++ b/docs/zh-Hans/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

看清应用的网络活动。

brew install --cask xinbetween/tap/flowlight

-

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

+

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

@@ -611,7 +611,7 @@

知道什么离开了你的 Mac。

diff --git a/docs/zh-Hans/privacy/index.html b/docs/zh-Hans/privacy/index.html index 260d0b4..5a60c90 100644 --- a/docs/zh-Hans/privacy/index.html +++ b/docs/zh-Hans/privacy/index.html @@ -168,7 +168,7 @@

联系方式

diff --git a/docs/zh-Hant/about/index.html b/docs/zh-Hant/about/index.html index 9a6a4ad..7ed8441 100644 --- a/docs/zh-Hant/about/index.html +++ b/docs/zh-Hant/about/index.html @@ -138,7 +138,7 @@

致謝

diff --git a/docs/zh-Hant/docs/index.html b/docs/zh-Hant/docs/index.html index 5362e32..c74c06c 100644 --- a/docs/zh-Hant/docs/index.html +++ b/docs/zh-Hant/docs/index.html @@ -62,7 +62,7 @@

說明文件

使用 Flowlight

-

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

+

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

diff --git a/docs/zh-Hant/index.html b/docs/zh-Hant/index.html index c9d957b..aa97154 100644 --- a/docs/zh-Hant/index.html +++ b/docs/zh-Hant/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

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

-

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

+

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

@@ -612,7 +612,7 @@

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

diff --git a/docs/zh-Hant/privacy/index.html b/docs/zh-Hant/privacy/index.html index 22c4f41..bde22cc 100644 --- a/docs/zh-Hant/privacy/index.html +++ b/docs/zh-Hant/privacy/index.html @@ -168,7 +168,7 @@

聯絡

diff --git a/project.yml b/project.yml index 0e04614..c27d634 100644 --- a/project.yml +++ b/project.yml @@ -11,7 +11,7 @@ settings: DEVELOPMENT_TEAM: "" CODE_SIGN_STYLE: Automatic ENABLE_HARDENED_RUNTIME: YES - MARKETING_VERSION: "0.8.1" + MARKETING_VERSION: "0.8.2" CURRENT_PROJECT_VERSION: "21" targets: Flowlight: diff --git a/site/pages/releases.html b/site/pages/releases.html index 53352c7..037b59d 100644 --- a/site/pages/releases.html +++ b/site/pages/releases.html @@ -13,7 +13,30 @@

What's new

-

0.8.1

Latest
+

0.8.2

Latest
+
    +
  • An update is verified before it is installed. The checksum comparison treated + "no checksum published", "no line for this disk image" and "the file didn't parse" as a match — + every way of not knowing was read as knowing it was right. It now requires a published checksum and + refuses the download without one.
  • +
  • And it has to be signed by us. The installer checked the downloaded app's + version and bundle identifier — strings inside the disk image that nothing signs — and then removed + the quarantine flag, which is what would have made macOS check the signature on first launch. + Updates are now validated against Apple's anchor and the Team ID of the running copy: an update has + to come from whoever signed the Flowlight asking for it.
  • +
  • A model endpoint can't take your key over plain HTTP. A compatible hosted + endpoint sent an API key and a question about your own traffic to whatever address was typed. + https:// is now required, except for loopback and private addresses, where the model is + running on your own machine or network.
  • +
  • Credential headers are recognised by their words, not by a list. Redaction knew + the common names, so X-Access-Key or a vendor's own spelling was written to disk under + a promise that says API keys are never stored. Anything whose name carries key, token, secret, + auth, credential, session or signature is redacted now.
  • +
+

Downloads and checksums →

+
+
+

0.8.1

  • Flowlight stops working when you are not looking at it. It was rebuilding the live chart and the per-app list every second whenever a window existed — including when that window was