diff --git a/Flowlight/Plugins/PluginEngine.swift b/Flowlight/Plugins/PluginEngine.swift index 688c9f44..5e7c53b3 100644 --- a/Flowlight/Plugins/PluginEngine.swift +++ b/Flowlight/Plugins/PluginEngine.swift @@ -38,7 +38,7 @@ enum PluginEngine { let enabled = Dictionary(uniqueKeysWithValues: manifests.map { ($0.id, $0.enabled) }) let context = PluginContext(exchange: exchange) return builtInRules.flatMap { rule -> [PluginFindingDraft] in - guard enabled[rule.manifest.id] ?? rule.manifest.enabled else { return [] } + guard enabled[rule.manifest.id] == true else { return [] } return rule.evaluate(context) } } @@ -48,8 +48,10 @@ enum PluginEngine { for builtIn in builtInManifests { if var existing = byID[builtIn.id] { let enabled = existing.enabled + let configuration = existing.configuration existing = builtIn existing.enabled = enabled + existing.configuration = configuration byID[builtIn.id] = existing } else { byID[builtIn.id] = builtIn diff --git a/Flowlight/Plugins/PluginModels.swift b/Flowlight/Plugins/PluginModels.swift index 72fea8cc..d5285432 100644 --- a/Flowlight/Plugins/PluginModels.swift +++ b/Flowlight/Plugins/PluginModels.swift @@ -14,6 +14,7 @@ struct PluginManifest: Codable, Identifiable, Equatable, Sendable { var ruleVersion: Int = 1 var privacySummary: String var capabilities: [Capability] = [.annotate] + var configuration: [String: String] = [:] enum Kind: String, Codable, Sendable { case traffic, llmMCP } enum Source: String, Codable, Sendable { case builtIn, installed } @@ -38,7 +39,8 @@ struct PluginManifest: Codable, Identifiable, Equatable, Sendable { init(id: String, name: String, version: String, kind: Kind, enabled: Bool = true, source: Source = .builtIn, publisher: Publisher = .official, guardrailProvider: GuardrailProvider = .flowlight, description: String, - ruleVersion: Int = 1, privacySummary: String, capabilities: [Capability] = [.annotate]) { + ruleVersion: Int = 1, privacySummary: String, capabilities: [Capability] = [.annotate], + configuration: [String: String] = [:]) { self.id = id self.name = name self.version = version @@ -51,10 +53,11 @@ struct PluginManifest: Codable, Identifiable, Equatable, Sendable { self.ruleVersion = ruleVersion self.privacySummary = privacySummary self.capabilities = capabilities + self.configuration = configuration } private enum CodingKeys: String, CodingKey { - case id, name, version, kind, enabled, source, publisher, guardrailProvider, description, ruleVersion, privacySummary, capabilities + case id, name, version, kind, enabled, source, publisher, guardrailProvider, description, ruleVersion, privacySummary, capabilities, configuration } init(from decoder: Decoder) throws { @@ -71,9 +74,28 @@ struct PluginManifest: Codable, Identifiable, Equatable, Sendable { ruleVersion = try values.decodeIfPresent(Int.self, forKey: .ruleVersion) ?? 1 privacySummary = try values.decode(String.self, forKey: .privacySummary) capabilities = try values.decodeIfPresent([Capability].self, forKey: .capabilities) ?? [.annotate] + configuration = try values.decodeIfPresent([String: String].self, forKey: .configuration) ?? [:] } } +struct PluginPackage: Codable, Equatable, Sendable { + var manifest: PluginManifest + + init(manifest: PluginManifest) { + self.manifest = manifest + } + + init(from decoder: Decoder) throws { + if let package = try? decoder.container(keyedBy: CodingKeys.self), package.contains(.manifest) { + manifest = try package.decode(PluginManifest.self, forKey: .manifest) + } else { + manifest = try PluginManifest(from: decoder) + } + } + + private enum CodingKeys: String, CodingKey { case manifest } +} + struct PluginEvidence: Codable, Equatable, Sendable { var label: String var value: String diff --git a/Flowlight/Plugins/PluginStore.swift b/Flowlight/Plugins/PluginStore.swift index 94b69aa8..68f0b8b4 100644 --- a/Flowlight/Plugins/PluginStore.swift +++ b/Flowlight/Plugins/PluginStore.swift @@ -1,5 +1,27 @@ import Foundation +struct PluginImportResult: Sendable { + var manifest: PluginManifest + var installed: Bool +} + +struct PluginBackfillResult: Sendable { + var pluginID: String? + var findingCount: Int +} + +enum PluginPackageError: LocalizedError, Equatable { + case reservedBuiltInID + case invalidID + + var errorDescription: String? { + switch self { + case .reservedBuiltInID: return L("Installed plugin IDs cannot reuse a built-in plugin ID.") + case .invalidID: return L("Plugin IDs must contain only letters, numbers, dots, underscores, and hyphens.") + } + } +} + @MainActor final class PluginStore: ObservableObject { @Published private(set) var manifests: [PluginManifest] = [] @@ -36,6 +58,64 @@ final class PluginStore: ObservableObject { version += 1 } + func updateConfiguration(_ manifest: PluginManifest, configuration: [String: String]) { + guard let index = manifests.firstIndex(where: { $0.id == manifest.id }) else { return } + manifests[index].configuration = configuration + db?.async { try $0.updatePluginConfiguration(id: manifest.id, configuration: configuration) } + version += 1 + } + + func importPackage(data: Data, completion: @escaping @MainActor (Result) -> Void) { + guard let db else { return } + db.async { [weak self] db in + do { + var manifest = try JSONDecoder().decode(PluginPackage.self, from: data).manifest + guard manifest.id.range(of: #"^[A-Za-z0-9_.-]+$"#, options: .regularExpression) != nil else { throw PluginPackageError.invalidID } + guard !PluginEngine.builtInManifests.contains(where: { $0.id == manifest.id }) else { throw PluginPackageError.reservedBuiltInID } + manifest.source = .installed + if manifest.publisher != .official { manifest.publisher = .thirdParty } + if manifest.capabilities.isEmpty { manifest.capabilities = [.annotate] } + try db.savePluginManifest(manifest) + let merged = PluginEngine.mergedManifests(try db.loadPluginManifests()) + Task { @MainActor in + self?.manifests = merged + self?.version += 1 + completion(.success(PluginImportResult(manifest: manifest, installed: true))) + } + } catch { + Task { @MainActor in completion(.failure(error)) } + } + } + } + + func exportPackage(_ manifest: PluginManifest) throws -> Data { + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + return try encoder.encode(PluginPackage(manifest: manifest)) + } + + func removeInstalled(_ manifest: PluginManifest) { + guard manifest.source == .installed else { return } + manifests.removeAll { $0.id == manifest.id } + db?.async { try $0.deleteInstalledPlugin(id: manifest.id) } + version += 1 + } + + func backfill(_ manifest: PluginManifest? = nil, completion: @escaping @MainActor (Result) -> Void) { + guard let db else { return } + db.async { [weak self] db in + do { + let count = try db.backfillPluginFindings(pluginID: manifest?.id) + Task { @MainActor in + self?.version += 1 + completion(.success(PluginBackfillResult(pluginID: manifest?.id, findingCount: count))) + } + } catch { + Task { @MainActor in completion(.failure(error)) } + } + } + } + func clearFindings() { db?.async { [weak self] db in try db.clearPluginFindings() diff --git a/Flowlight/Storage/TrafficDatabase.swift b/Flowlight/Storage/TrafficDatabase.swift index 9cdd715e..5c9c60e4 100644 --- a/Flowlight/Storage/TrafficDatabase.swift +++ b/Flowlight/Storage/TrafficDatabase.swift @@ -1025,6 +1025,41 @@ final class TrafficDatabase: @unchecked Sendable { } } + func updatePluginConfiguration(id: String, configuration: [String: String]) throws { + guard var manifest = try loadPluginManifests().first(where: { $0.id == id }) else { return } + manifest.configuration = configuration + try savePluginManifest(manifest) + } + + func deleteInstalledPlugin(id: String) throws { + guard let manifest = try loadPluginManifests().first(where: { $0.id == id }), manifest.source == .installed else { return } + try conn.transaction { + try conn.run("DELETE FROM plugin_findings WHERE plugin_id = ?", [.text(id)]) + try conn.run("DELETE FROM plugins WHERE id = ?", [.text(id)]) + } + } + + @discardableResult + func backfillPluginFindings(pluginID: String? = nil, since: Date = Date().addingTimeInterval(-14 * 86400), limit: Int = 500) throws -> Int { + let manifests = PluginEngine.mergedManifests(try loadPluginManifests()) + let active = pluginID.map { id in manifests.filter { $0.id == id } } ?? manifests + let builtInIDs = Set(PluginEngine.builtInManifests.map(\.id)) + let evaluable = active.filter { builtInIDs.contains($0.id) } + guard !evaluable.isEmpty else { return 0 } + let exchanges = try exchanges(since: since, limit: limit) + var inserted = 0 + try conn.transaction { + if let pluginID { try conn.run("DELETE FROM plugin_findings WHERE plugin_id = ?", [.text(pluginID)]) } + else { try conn.run("DELETE FROM plugin_findings WHERE plugin_id IN (\(Array(repeating: "?", count: builtInIDs.count).joined(separator: ",")))", builtInIDs.map(SQLValue.text)) } + for exchange in exchanges { + let before = conn.changes + try insertPluginFindings(PluginEngine.evaluate(exchange, manifests: evaluable), forExchangeID: exchange.id ?? 0) + inserted += max(0, conn.changes - before) + } + } + return inserted + } + func insertPluginFindings(_ findings: [PluginFindingDraft], forExchangeID exchangeID: Int64) throws { guard !findings.isEmpty else { return } let encoder = JSONEncoder() diff --git a/Flowlight/UI/PluginSettingsView.swift b/Flowlight/UI/PluginSettingsView.swift index bd4c7380..f5ba69ac 100644 --- a/Flowlight/UI/PluginSettingsView.swift +++ b/Flowlight/UI/PluginSettingsView.swift @@ -1,31 +1,61 @@ +import AppKit import SwiftUI +import UniformTypeIdentifiers struct PluginSettingsView: View { @EnvironmentObject var monitor: TrafficMonitor @State private var showClearConfirmation = false + @State private var message: String? + @State private var configuring: PluginManifest? + @State private var removing: PluginManifest? var body: some View { Form { Section { Text(L("Plugins add advisory findings to inspected exchanges. They do not block, rewrite, or send traffic anywhere by themselves.")) .font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true) + HStack(spacing: 8) { + Button(L("Import Package…"), action: importPackage) + Button(L("Backfill Findings")) { backfill(nil) } + Button(L("Clear plugin findings…"), role: .destructive) { showClearConfirmation = true } + } + if let message { + Text(message).font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true) + } + } footer: { + Text(L("Import reads a local plugin manifest package and stores it as installed metadata. Until sandboxed plugins ship, installed packages are visible and configurable but only Flowlight built-ins create findings.")) + .font(.caption).foregroundStyle(.secondary) } pluginSection(L("Official"), plugins: monitor.plugins.officialPlugins, empty: L("No official plugins are installed.")) pluginSection(L("Third-party"), plugins: monitor.plugins.thirdPartyPlugins, - empty: L("Third-party plugins are not installed in this version.")) - Section { - Button(L("Clear plugin findings…"), role: .destructive) { showClearConfirmation = true } - .help(L("Delete retained plugin annotations without deleting recorded requests or plugin settings")) - } + empty: L("No third-party plugin packages are installed.")) } .formStyle(.grouped) - .frame(height: 520) + .frame(height: 620) .confirmationDialog(L("Clear plugin findings?"), isPresented: $showClearConfirmation, titleVisibility: .visible) { Button(L("Clear findings"), role: .destructive) { monitor.clearPluginFindings() } } message: { Text(L("Recorded requests, response bodies, rules, guardrails, and plugin enable settings stay in place.")) } + .confirmationDialog(L("Remove installed plugin?"), isPresented: Binding(get: { removing != nil }, set: { if !$0 { removing = nil } }), titleVisibility: .visible) { + if let plugin = removing { + Button(L("Remove %@", plugin.name), role: .destructive) { + monitor.plugins.removeInstalled(plugin) + removing = nil + } + } + } message: { + if let plugin = removing { + Text(L("%@ and its retained findings will be removed. Built-in plugins cannot be removed.", plugin.name)) + } + } + .sheet(item: $configuring) { plugin in + PluginConfigurationSheet(plugin: plugin) { configuration in + monitor.plugins.updateConfiguration(plugin, configuration: configuration) + message = L("Saved configuration for %@.", plugin.name) + } + } } private func pluginSection(_ title: String, plugins: [PluginManifest], empty: String) -> some View { @@ -34,35 +64,164 @@ struct PluginSettingsView: View { Text(empty).font(.caption).foregroundStyle(.secondary) } else { ForEach(plugins) { plugin in - PluginSettingsRow(plugin: plugin) { enabled in + PluginSettingsRow(plugin: plugin, setEnabled: { enabled in monitor.plugins.setEnabled(plugin, enabled) - } + }, configure: { + configuring = plugin + }, exportPackage: { + exportPackage(plugin) + }, backfill: { + backfill(plugin) + }, remove: plugin.source == .installed ? { + removing = plugin + } : nil) } } } } + + private func importPackage() { + let panel = NSOpenPanel() + panel.allowedContentTypes = [.json] + panel.allowsMultipleSelection = false + panel.canChooseDirectories = false + panel.prompt = L("Import") + panel.message = L("Choose a Flowlight inspection plugin manifest or package JSON file.") + guard panel.runModal() == .OK, let url = panel.url else { return } + do { + let data = try Data(contentsOf: url) + monitor.plugins.importPackage(data: data) { result in + switch result { + case .success(let imported): message = L("Installed %@.", imported.manifest.name) + case .failure(let error): message = L("Import failed: %@", error.localizedDescription) + } + } + } catch { + message = L("Import failed: %@", error.localizedDescription) + } + } + + private func exportPackage(_ plugin: PluginManifest) { + do { + let data = try monitor.plugins.exportPackage(plugin) + let panel = NSSavePanel() + panel.allowedContentTypes = [.json] + panel.nameFieldStringValue = "\(plugin.id)-plugin.json" + if panel.runModal() == .OK, let url = panel.url { + try data.write(to: url, options: .atomic) + message = L("Exported %@.", plugin.name) + } + } catch { + message = L("Export failed: %@", error.localizedDescription) + } + } + + private func backfill(_ plugin: PluginManifest?) { + monitor.plugins.backfill(plugin) { result in + switch result { + case .success(let result): + message = plugin.map { L("Backfilled %@: %lld findings.", $0.name, result.findingCount) } + ?? L("Backfilled built-in plugins: %lld findings.", result.findingCount) + case .failure(let error): message = L("Backfill failed: %@", error.localizedDescription) + } + } + } } private struct PluginSettingsRow: View { var plugin: PluginManifest var setEnabled: (Bool) -> Void + var configure: () -> Void + var exportPackage: () -> Void + var backfill: () -> Void + var remove: (() -> Void)? var body: some View { - Toggle(isOn: Binding(get: { plugin.enabled }, set: setEnabled)) { - VStack(alignment: .leading, spacing: 4) { - HStack(spacing: 6) { - Text(plugin.name).font(.callout.bold()) - PluginPill(plugin.kind.title) - PluginPill(plugin.guardrailProvider.title) - if plugin.capabilities.contains(.suggestGuardrail) { PluginPill(L("Suggests guardrails")) } + VStack(alignment: .leading, spacing: 8) { + Toggle(isOn: Binding(get: { plugin.enabled }, set: setEnabled)) { + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 6) { + Text(plugin.name).font(.callout.bold()) + PluginPill(plugin.kind.title) + PluginPill(plugin.guardrailProvider.title) + PluginPill(plugin.source.title) + if plugin.capabilities.contains(.suggestGuardrail) { PluginPill(L("Suggests guardrails")) } + } + Text(plugin.description).font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true) + Text(plugin.privacySummary).font(.caption2).foregroundStyle(.tertiary).fixedSize(horizontal: false, vertical: true) + Text(L("%@ · v%@ · rule v%lld", plugin.publisher.title, plugin.version, plugin.ruleVersion)) + .font(.caption2).foregroundStyle(.tertiary) + if !plugin.configuration.isEmpty { + Text(L("Configured: %@", plugin.configuration.keys.sorted().joined(separator: ", "))) + .font(.caption2).foregroundStyle(.tertiary) + } } - Text(plugin.description).font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true) - Text(plugin.privacySummary).font(.caption2).foregroundStyle(.tertiary).fixedSize(horizontal: false, vertical: true) - Text(L("%@ · v%@ · rule v%lld", plugin.publisher.title, plugin.version, plugin.ruleVersion)) - .font(.caption2).foregroundStyle(.tertiary) } + .toggleStyle(.switch) + HStack(spacing: 8) { + Button(L("Configure…"), action: configure) + Button(L("Export…"), action: exportPackage) + Button(L("Backfill"), action: backfill) + if let remove { Button(L("Remove"), role: .destructive, action: remove) } + } + .font(.caption) } - .toggleStyle(.switch) + .padding(.vertical, 2) + } +} + +private struct PluginConfigurationSheet: View { + @Environment(\.dismiss) private var dismiss + var plugin: PluginManifest + var save: ([String: String]) -> Void + @State private var rows: [ConfigurationRow] + + init(plugin: PluginManifest, save: @escaping ([String: String]) -> Void) { + self.plugin = plugin + self.save = save + _rows = State(initialValue: plugin.configuration.sorted { $0.key < $1.key }.map { ConfigurationRow(key: $0.key, value: $0.value) }) + } + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text(L("Configure %@", plugin.name)).font(.headline) + Text(L("Configuration is stored locally as string key/value metadata. It does not grant a plugin network, file, or traffic mutation access.")) + .font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true) + ScrollView { + VStack(spacing: 8) { + ForEach($rows) { $row in + HStack(spacing: 8) { + TextField(L("Key"), text: $row.key).textFieldStyle(.roundedBorder) + TextField(L("Value"), text: $row.value).textFieldStyle(.roundedBorder) + Button { rows.removeAll { $0.id == row.id } } label: { Image(systemName: "minus.circle") } + .buttonStyle(.borderless).help(L("Remove this setting")) + } + } + } + } + HStack { + Button(L("Add Setting")) { rows.append(ConfigurationRow()) } + Spacer() + Button(L("Cancel")) { dismiss() } + Button(L("Save")) { + save(Dictionary(uniqueKeysWithValues: rows.compactMap { row in + let key = row.key.trimmingCharacters(in: .whitespacesAndNewlines) + guard !key.isEmpty else { return nil } + return (key, row.value) + })) + dismiss() + } + .keyboardShortcut(.defaultAction) + } + } + .padding(20) + .frame(width: 520, height: 360) + } + + private struct ConfigurationRow: Identifiable { + let id = UUID() + var key = "" + var value = "" } } @@ -86,3 +245,12 @@ private extension PluginManifest.Publisher { } } } + +private extension PluginManifest.Source { + var title: String { + switch self { + case .builtIn: return L("Built-in") + case .installed: return L("Installed") + } + } +} diff --git a/FlowlightTests/PluginTests.swift b/FlowlightTests/PluginTests.swift index 38369643..851f1ef2 100644 --- a/FlowlightTests/PluginTests.swift +++ b/FlowlightTests/PluginTests.swift @@ -82,10 +82,65 @@ final class PluginTests: XCTestCase { description: "Detects and masks PII.", privacySummary: "Uses redacted inspection metadata.") manifest.source = .installed manifest.publisher = .official + manifest.configuration = ["policy": "default"] let data = try JSONEncoder().encode(manifest) let decoded = try JSONDecoder().decode(PluginManifest.self, from: data) XCTAssertEqual(decoded.publisher, .official) XCTAssertEqual(decoded.guardrailProvider, .presidio) + XCTAssertEqual(decoded.configuration["policy"], "default") XCTAssertEqual(PluginManifest.GuardrailProvider.prismaAIRS.title, "PANW Prisma AIRS") } + + func testPluginPackageAcceptsBareManifestAndWrappedPackage() throws { + let bare = Data(#"{"id":"thirdparty.test","name":"Test","version":"0.1.0","kind":"traffic","source":"installed","publisher":"thirdParty","guardrailProvider":"custom","description":"Test plugin.","privacySummary":"Uses metadata only.","capabilities":["annotate"]}"#.utf8) + let decodedBare = try JSONDecoder().decode(PluginPackage.self, from: bare) + XCTAssertEqual(decodedBare.manifest.id, "thirdparty.test") + + let wrapped = try JSONEncoder().encode(PluginPackage(manifest: decodedBare.manifest)) + let decodedWrapped = try JSONDecoder().decode(PluginPackage.self, from: wrapped) + XCTAssertEqual(decodedWrapped.manifest.id, "thirdparty.test") + } + + @MainActor + func testPluginStoreRejectsReservedBuiltInPackageID() { + let store = PluginStore() + let data = Data(#"{"id":"traffic.large-upload","name":"Pretend","version":"0.1.0","kind":"traffic","source":"installed","publisher":"thirdParty","guardrailProvider":"custom","description":"Pretend plugin.","privacySummary":"Uses metadata only.","capabilities":["annotate"]}"#.utf8) + let exp = expectation(description: "import completes") + store.attach(db: try! database()) + store.importPackage(data: data) { result in + if case .failure(let error as PluginPackageError) = result { + XCTAssertEqual(error, .reservedBuiltInID) + } else { + XCTFail("expected reserved built-in ID rejection") + } + exp.fulfill() + } + wait(for: [exp], timeout: 2) + } + + func testInstalledPluginConfigurationAndRemovalPersist() throws { + let db = try database() + var manifest = PluginManifest(id: "thirdparty.example", name: "Example", version: "0.1.0", kind: .traffic, + source: .installed, publisher: .thirdParty, guardrailProvider: .custom, + description: "Example plugin.", privacySummary: "Uses metadata only.") + try db.savePluginManifest(manifest) + try db.updatePluginConfiguration(id: manifest.id, configuration: ["mode": "audit"]) + manifest = try XCTUnwrap(db.loadPluginManifests().first { $0.id == "thirdparty.example" }) + XCTAssertEqual(manifest.configuration["mode"], "audit") + + try db.deleteInstalledPlugin(id: manifest.id) + XCTAssertNil(try db.loadPluginManifests().first { $0.id == "thirdparty.example" }) + } + + func testBackfillRecreatesBuiltInFindingsForRetainedExchanges() throws { + let db = try database() + let id = try db.insertExchange(exchange()) + XCTAssertFalse(try db.pluginFindings(exchangeIDs: [id]).isEmpty) + try db.clearPluginFindings() + XCTAssertTrue(try db.pluginFindings(exchangeIDs: [id], visibleOnly: false).isEmpty) + let count = try db.backfillPluginFindings(pluginID: "traffic.large-upload") + XCTAssertEqual(count, 1) + let findings = try db.pluginFindings(exchangeIDs: [id], visibleOnly: false) + XCTAssertEqual(findings[id]?.map(\.pluginID), ["traffic.large-upload"]) + } } diff --git a/docs/404.html b/docs/404.html index f4747c3e..35dfc093 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 27945bd1..42875ad4 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 e57b0d00..8491efa1 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 2efdc766..34ae5452 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.0, macOS 15 oder neuer.

+

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

diff --git a/docs/de/index.html b/docs/de/index.html index 78625a43..df647c1a 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.0macOS 15+UniversalGPL-3.0Keine Telemetrie

+

v0.14.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 1f868036..7f1b1601 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 e821bfa3..464cf555 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 737bb9ae..0906bc49 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.0, macOS 15 or later.

+

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

diff --git a/docs/es/about/index.html b/docs/es/about/index.html index 847fa3bf..ec16ceb5 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 07e3a0db..281effa6 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.0, macOS 15 o posterior.

+

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

diff --git a/docs/es/index.html b/docs/es/index.html index c6dde3c8..3d5a7d38 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.0macOS 15+UniversalGPL-3.0Sin telemetría

+

v0.14.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 6047ccad..a7311c0d 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 8f3a2d76..d79ecbd6 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 5c67fd0a..5135dec8 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 3b8ad429..afd42d84 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.0, macOS 15 ou version ultérieure.

+

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

diff --git a/docs/fr/index.html b/docs/fr/index.html index f46c68d3..73e409ad 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.0macOS 15+UniverselGPL-3.0Sans télémétrie

+

v0.14.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 d8d2144f..1cf8af55 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 d5ee24c5..be339c2e 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 e986f7ae..77ac8202 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.0macOS 15+UniversalGPL-3.0No telemetry

+

v0.14.1macOS 15+UniversalGPL-3.0No telemetry

@@ -580,7 +580,7 @@

Know what leaves your Mac.

diff --git a/docs/it/about/index.html b/docs/it/about/index.html index e762e6ea..f7f490a7 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 4908ce5d..b85d191a 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.0, macOS 15 o successivo.

+

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

diff --git a/docs/it/index.html b/docs/it/index.html index dd96b8c0..5db92f90 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.0macOS 15+UniversaleGPL-3.0Nessuna telemetria

+

v0.14.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 8e4253f2..4f9385ce 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 397244e5..a5741209 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 5e4743de..14277e13 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 e1b9711d..b4291306 100644 --- a/docs/ja/docs/index.html +++ b/docs/ja/docs/index.html @@ -76,7 +76,7 @@

ドキュメント

Flowlight の使い方

-

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

+

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

diff --git a/docs/ja/index.html b/docs/ja/index.html index 4f6644aa..1e5125b3 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.0macOS 15+ユニバーサルGPL-3.0テレメトリなし

+

v0.14.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 539e8186..5651ecae 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 f5744174..aab950c3 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 3637c4b9..8dc9de4c 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 7d3adec0..a024dea2 100644 --- a/docs/ko/docs/index.html +++ b/docs/ko/docs/index.html @@ -76,7 +76,7 @@

문서

Flowlight 사용하기

-

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

+

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

diff --git a/docs/ko/index.html b/docs/ko/index.html index c86e5fa2..fae241d4 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.0macOS 15+유니버설GPL-3.0텔레메트리 없음

+

v0.14.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 7c84b4bb..13be8f2d 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 e751faaa..741e4654 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 27555635..9fe7586e 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.0, macOS 15 or later. + Everything from the first launch to tuning the agent rules. Flowlight 0.14.1, macOS 15 or later. On this page @@ -853,7 +853,7 @@ Understand your AI agents. brew install --cask xinbetween/tap/flowlightCopy - v0.14.0macOS 15+UniversalGPL-3.0No telemetry + v0.14.1macOS 15+UniversalGPL-3.0No telemetry connectionslive @@ -1323,7 +1323,7 @@ Plugins Flowlight plugins are scoped to HTTPS inspection. They run after a request and response have already been captured, redacted, clipped, attributed to an app or agent, and written to the local database. A plugin can add findings and suggest ordinary Flowlight guardrails, but it does not block, rewrite, mock, or transform traffic by itself. - The first shipped plugins are official built-ins. The template package below is for designing third-party or official add-on packs against the manifest and finding format while the installed plugin loader is still being hardened. + Flowlight can import and export local plugin manifest packages from Settings. Installed packages are visible and configurable today; execution remains limited to official built-in rules until sandboxed advisory plugins ship. Manifest @@ -1396,14 +1396,14 @@ Plugins Template package - The starter package lives at templates/inspection-plugin. It includes a manifest, sample findings, a policy checklist and fixtures for ordinary traffic and LLM/MCP guardrail plugins. + The starter package lives at templates/inspection-plugin. It includes a manifest, sample findings, a policy checklist and fixtures for ordinary traffic and LLM/MCP guardrail plugins. Import the manifest JSON from Settings › Plugins, then export it again to share the stored package shape. cp -R templates/inspection-plugin my-flowlight-plugin cd my-flowlight-plugin -python3 -m json.tool manifest.json -python3 validate.py +node -e "JSON.parse(require('fs').readFileSync('manifest.json'))" +node validate.js - The validator is deliberately local and boring: it checks manifest fields, provider names, capability names, evidence size limits and the absence of common secret-looking values in sample findings. + The JavaScript validator is deliberately local and boring: it checks manifest fields, provider names, capability names, evidence size limits and the absence of common secret-looking values in sample findings. Review checklist @@ -1536,9 +1536,23 @@ Releases Downloads, checksums and full notes for each version are on GitHub Releases. - 0.14.0 + 0.14.1 October 9, 2026Latest + Install local plugin packages. Settings › Plugins can now import and export Flowlight plugin + manifest packages, keep installed plugins split by official or third-party publisher, and reject packages that + try to reuse a reserved built-in plugin ID. + + Configure and maintain plugin records. Plugin packages can store local string key/value + configuration, installed packages can be removed with their retained findings, and built-in plugins preserve + enable/configuration state while the app updates their manifest metadata. + + Backfill advisory findings. The Plugins tab can rerun official built-in rules over retained + inspected exchanges so newly enabled or updated plugins can annotate recent history without recapturing traffic. + + 0.14.0 +October 9, 2026 + Official inspection plugins. Flowlight now derives advisory plugin findings from inspected exchanges after capture, redaction and attribution. The first official built-ins cover large uploads, destructive-looking HTTP requests, credential-like header names, direct HTTP MCP activity, provider-run MCP diff --git a/docs/llms.txt b/docs/llms.txt index b4bf8a63..970729ce 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.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.14.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 a0a9f172..cc349946 100644 --- a/docs/plugins/index.html +++ b/docs/plugins/index.html @@ -75,7 +75,7 @@

Write inspection plugins

The trust boundary

Flowlight plugins are scoped to HTTPS inspection. They run after a request and response have already been captured, redacted, clipped, attributed to an app or agent, and written to the local database. A plugin can add findings and suggest ordinary Flowlight guardrails, but it does not block, rewrite, mock, or transform traffic by itself.

-

The first shipped plugins are official built-ins. The template package below is for designing third-party or official add-on packs against the manifest and finding format while the installed plugin loader is still being hardened.

+

Flowlight can import and export local plugin manifest packages from Settings. Installed packages are visible and configurable today; execution remains limited to official built-in rules until sandboxed advisory plugins ship.

Manifest

A manifest tells Flowlight what the plugin is, who publishes it, what kind of inspected data it uses, and whether it can suggest guardrails.

@@ -133,12 +133,12 @@

Supported guardrail providers

Template package

-

The starter package lives at templates/inspection-plugin. It includes a manifest, sample findings, a policy checklist and fixtures for ordinary traffic and LLM/MCP guardrail plugins.

+

The starter package lives at templates/inspection-plugin. It includes a manifest, sample findings, a policy checklist and fixtures for ordinary traffic and LLM/MCP guardrail plugins. Import the manifest JSON from Settings › Plugins, then export it again to share the stored package shape.

cp -R templates/inspection-plugin my-flowlight-plugin
 cd my-flowlight-plugin
-python3 -m json.tool manifest.json
-python3 validate.py
-

The validator is deliberately local and boring: it checks manifest fields, provider names, capability names, evidence size limits and the absence of common secret-looking values in sample findings.

+node -e "JSON.parse(require('fs').readFileSync('manifest.json'))" +node validate.js +

The JavaScript validator is deliberately local and boring: it checks manifest fields, provider names, capability names, evidence size limits and the absence of common secret-looking values in sample findings.

Review checklist

    @@ -183,7 +183,7 @@

    Review checklist

    diff --git a/docs/privacy/index.html b/docs/privacy/index.html index 0add57e3..d4372a03 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 d8883b61..dee83952 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 069e5af9..587e9287 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.0, macOS 15 ou posterior.

    +

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

    diff --git a/docs/pt-PT/index.html b/docs/pt-PT/index.html index bc0afe68..8aeac6eb 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.0macOS 15+UniversalGPL-3.0Sem telemetria

    +

    v0.14.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 67068e7b..5dd7bd13 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 81f82472..7043e7bf 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 413fca4b..041a871c 100644 --- a/docs/releases/index.html +++ b/docs/releases/index.html @@ -61,7 +61,21 @@

    What's new

    -

    0.14.0

    Latest
    +

    0.14.1

    Latest
    +
      +
    • Install local plugin packages. Settings › Plugins can now import and export Flowlight plugin + manifest packages, keep installed plugins split by official or third-party publisher, and reject packages that + try to reuse a reserved built-in plugin ID.
    • +
    • Configure and maintain plugin records. Plugin packages can store local string key/value + configuration, installed packages can be removed with their retained findings, and built-in plugins preserve + enable/configuration state while the app updates their manifest metadata.
    • +
    • Backfill advisory findings. The Plugins tab can rerun official built-in rules over retained + inspected exchanges so newly enabled or updated plugins can annotate recent history without recapturing traffic.
    • +
    +
    + +
    +

    0.14.0

    • Official inspection plugins. Flowlight now derives advisory plugin findings from inspected exchanges after capture, redaction and attribution. The first official built-ins cover large uploads, @@ -1141,7 +1155,7 @@

      What's new

    diff --git a/docs/threat-model/index.html b/docs/threat-model/index.html index acc82e02..c6c7028d 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 b4b1fb02..4d14bff8 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 06629415..7f249ddc 100644 --- a/docs/zh-Hans/docs/index.html +++ b/docs/zh-Hans/docs/index.html @@ -76,7 +76,7 @@

    文档

    使用 Flowlight

    -

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

    +

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

    diff --git a/docs/zh-Hans/index.html b/docs/zh-Hans/index.html index e2b771df..acb32d09 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.0macOS 15+通用架构GPL-3.0无遥测

    +

    v0.14.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 c3e380fb..6ee75087 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 57a64403..37c06da9 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 d02fd5a6..1eb56637 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 a9300ebc..93bd8751 100644 --- a/docs/zh-Hant/docs/index.html +++ b/docs/zh-Hant/docs/index.html @@ -76,7 +76,7 @@

    說明文件

    使用 Flowlight

    -

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

    +

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

    diff --git a/docs/zh-Hant/index.html b/docs/zh-Hant/index.html index 3636e889..7ae94d6a 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.0macOS 15+通用架構GPL-3.0無遙測

    +

    v0.14.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 71bb7833..fd60d2f2 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 bafba42f..b6a6cb93 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 ad0d9e0d..721bdbe9 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.0" - CURRENT_PROJECT_VERSION: "30" + MARKETING_VERSION: "0.14.1" + CURRENT_PROJECT_VERSION: "31" targets: Flowlight: type: application diff --git a/site/pages/plugins.html b/site/pages/plugins.html index d67e6a68..2761ab2e 100644 --- a/site/pages/plugins.html +++ b/site/pages/plugins.html @@ -27,7 +27,7 @@

    Write inspection plugins

    The trust boundary

    Flowlight plugins are scoped to HTTPS inspection. They run after a request and response have already been captured, redacted, clipped, attributed to an app or agent, and written to the local database. A plugin can add findings and suggest ordinary Flowlight guardrails, but it does not block, rewrite, mock, or transform traffic by itself.

    -

    The first shipped plugins are official built-ins. The template package below is for designing third-party or official add-on packs against the manifest and finding format while the installed plugin loader is still being hardened.

    +

    Flowlight can import and export local plugin manifest packages from Settings. Installed packages are visible and configurable today; execution remains limited to official built-in rules until sandboxed advisory plugins ship.

    Manifest

    A manifest tells Flowlight what the plugin is, who publishes it, what kind of inspected data it uses, and whether it can suggest guardrails.

    @@ -85,12 +85,12 @@

    Supported guardrail providers

    Template package

    -

    The starter package lives at templates/inspection-plugin. It includes a manifest, sample findings, a policy checklist and fixtures for ordinary traffic and LLM/MCP guardrail plugins.

    +

    The starter package lives at templates/inspection-plugin. It includes a manifest, sample findings, a policy checklist and fixtures for ordinary traffic and LLM/MCP guardrail plugins. Import the manifest JSON from Settings › Plugins, then export it again to share the stored package shape.

    cp -R templates/inspection-plugin my-flowlight-plugin
     cd my-flowlight-plugin
    -python3 -m json.tool manifest.json
    -python3 validate.py
    -

    The validator is deliberately local and boring: it checks manifest fields, provider names, capability names, evidence size limits and the absence of common secret-looking values in sample findings.

    +node -e "JSON.parse(require('fs').readFileSync('manifest.json'))" +node validate.js +

    The JavaScript validator is deliberately local and boring: it checks manifest fields, provider names, capability names, evidence size limits and the absence of common secret-looking values in sample findings.

    Review checklist

      diff --git a/site/pages/releases.html b/site/pages/releases.html index a1b4e293..7f33e7fe 100644 --- a/site/pages/releases.html +++ b/site/pages/releases.html @@ -13,7 +13,21 @@

      What's new

      -

      0.14.0

      Latest
      +

      0.14.1

      Latest
      +
        +
      • Install local plugin packages. Settings › Plugins can now import and export Flowlight plugin + manifest packages, keep installed plugins split by official or third-party publisher, and reject packages that + try to reuse a reserved built-in plugin ID.
      • +
      • Configure and maintain plugin records. Plugin packages can store local string key/value + configuration, installed packages can be removed with their retained findings, and built-in plugins preserve + enable/configuration state while the app updates their manifest metadata.
      • +
      • Backfill advisory findings. The Plugins tab can rerun official built-in rules over retained + inspected exchanges so newly enabled or updated plugins can annotate recent history without recapturing traffic.
      • +
      +
      + +
      +

      0.14.0

      • Official inspection plugins. Flowlight now derives advisory plugin findings from inspected exchanges after capture, redaction and attribution. The first official built-ins cover large uploads, diff --git a/templates/inspection-plugin/README.md b/templates/inspection-plugin/README.md index ea7d109a..26936839 100644 --- a/templates/inspection-plugin/README.md +++ b/templates/inspection-plugin/README.md @@ -1,6 +1,6 @@ # Flowlight Inspection Plugin Template -This template describes an advisory Flowlight inspection plugin package. The first app release with plugins ships official built-in Swift plugins only; this package is the authoring shape for official add-on packs and future installed third-party plugins. +This template describes an advisory Flowlight inspection plugin package. Flowlight can import and export this manifest shape today; installed packages are visible and configurable, while executable plugin logic remains limited to official built-in Swift rules until the sandbox phase. ## Trust boundary @@ -10,9 +10,9 @@ Evidence must be bounded and redacted. Do not include raw request or response bo ## Files -- `manifest.json`: plugin identity, publisher category, guardrail provider, privacy summary and capabilities. +- `manifest.json`: plugin identity, publisher category, guardrail provider, privacy summary, optional `configuration` string map and capabilities. - `findings.example.json`: sample findings the plugin may produce. -- `validate.py`: local schema and privacy sanity checks for the template package. +- `validate.js`: local schema and privacy sanity checks for the template package. ## Supported guardrail providers @@ -28,5 +28,5 @@ Evidence must be bounded and redacted. Do not include raw request or response bo ## Validate ```sh -python3 validate.py +node validate.js ``` diff --git a/templates/inspection-plugin/validate.js b/templates/inspection-plugin/validate.js new file mode 100644 index 00000000..779b349c --- /dev/null +++ b/templates/inspection-plugin/validate.js @@ -0,0 +1,71 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); + +const root = __dirname; +const kinds = new Set(['traffic', 'llmMCP']); +const sources = new Set(['builtIn', 'installed']); +const publishers = new Set(['official', 'thirdParty']); +const providers = new Set(['flowlight', 'presidio', 'bedrock', 'lakera', 'aporia', 'noma', 'prismaAIRS', 'custom']); +const capabilities = new Set(['annotate', 'suggestGuardrail']); +const severities = new Set(['info', 'low', 'medium', 'high']); +const secretPattern = /(sk-[A-Za-z0-9_-]{8,}|bearer\s+\S+|api[_-]?key\s*[:=]|secret\s*[:=]|token\s*[:=])/i; + +function load(name) { + return JSON.parse(fs.readFileSync(path.join(root, name), 'utf8')); +} + +function fail(message) { + console.error(`error: ${message}`); + process.exit(1); +} + +function requireCondition(condition, message) { + if (!condition) fail(message); +} + +function isPlainObject(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function checkManifest(manifest) { + for (const key of ['id', 'name', 'version', 'kind', 'source', 'publisher', 'guardrailProvider', 'description', 'privacySummary', 'capabilities']) { + requireCondition(Object.prototype.hasOwnProperty.call(manifest, key), `manifest missing ${key}`); + } + requireCondition(/^[A-Za-z0-9_.-]+$/.test(manifest.id), 'manifest id must use letters, numbers, dots, underscores, and hyphens'); + requireCondition(kinds.has(manifest.kind), 'manifest kind must be traffic or llmMCP'); + requireCondition(sources.has(manifest.source), 'manifest source must be builtIn or installed'); + requireCondition(publishers.has(manifest.publisher), 'manifest publisher must be official or thirdParty'); + requireCondition(providers.has(manifest.guardrailProvider), 'unsupported guardrailProvider'); + requireCondition(Array.isArray(manifest.capabilities), 'manifest capabilities must be an array'); + requireCondition(manifest.capabilities.every(capability => capabilities.has(capability)), 'unsupported capability'); + requireCondition(manifest.privacySummary.length <= 500, 'privacySummary should stay concise'); + if (manifest.configuration !== undefined) { + requireCondition(isPlainObject(manifest.configuration), 'configuration must be an object'); + for (const [key, value] of Object.entries(manifest.configuration)) { + requireCondition(typeof value === 'string', `configuration ${key} must be a string`); + } + } +} + +function checkFindings(findings) { + requireCondition(Array.isArray(findings), 'findings.example.json must be an array'); + findings.forEach((finding, index) => { + requireCondition(severities.has(finding.severity), `finding ${index} has unsupported severity`); + for (const key of ['title', 'summary', 'evidence']) { + requireCondition(Object.prototype.hasOwnProperty.call(finding, key), `finding ${index} missing ${key}`); + } + requireCondition(finding.title.length <= 120, `finding ${index} title is too long`); + requireCondition(finding.summary.length <= 500, `finding ${index} summary is too long`); + requireCondition(Array.isArray(finding.evidence) && finding.evidence.length <= 8, `finding ${index} evidence must be a short array`); + requireCondition(!secretPattern.test(JSON.stringify(finding)), `finding ${index} appears to contain a secret-like value`); + finding.evidence.forEach(evidence => { + requireCondition((evidence.label || '').length <= 80, `finding ${index} evidence label is too long`); + requireCondition((evidence.value || '').length <= 240, `finding ${index} evidence value is too long`); + }); + }); +} + +checkManifest(load('manifest.json')); +checkFindings(load('findings.example.json')); +console.log('ok'); diff --git a/templates/inspection-plugin/validate.py b/templates/inspection-plugin/validate.py deleted file mode 100644 index 8000fa6e..00000000 --- a/templates/inspection-plugin/validate.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -import json -import re -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parent -KINDS = {"traffic", "llmMCP"} -SOURCES = {"builtIn", "installed"} -PUBLISHERS = {"official", "thirdParty"} -PROVIDERS = {"flowlight", "presidio", "bedrock", "lakera", "aporia", "noma", "prismaAIRS", "custom"} -CAPABILITIES = {"annotate", "suggestGuardrail"} -SEVERITIES = {"info", "low", "medium", "high"} -SECRET_RE = re.compile(r"(sk-[A-Za-z0-9_-]{8,}|bearer\s+\S+|api[_-]?key\s*[:=]|secret\s*[:=]|token\s*[:=])", re.I) - - -def load(name): - return json.loads((ROOT / name).read_text()) - - -def fail(message): - print(f"error: {message}", file=sys.stderr) - sys.exit(1) - - -def require(condition, message): - if not condition: - fail(message) - - -def check_manifest(manifest): - for key in ["id", "name", "version", "kind", "source", "publisher", "guardrailProvider", "description", "privacySummary", "capabilities"]: - require(key in manifest, f"manifest missing {key}") - require(manifest["kind"] in KINDS, "manifest kind must be traffic or llmMCP") - require(manifest["source"] in SOURCES, "manifest source must be builtIn or installed") - require(manifest["publisher"] in PUBLISHERS, "manifest publisher must be official or thirdParty") - require(manifest["guardrailProvider"] in PROVIDERS, "unsupported guardrailProvider") - require(set(manifest["capabilities"]).issubset(CAPABILITIES), "unsupported capability") - require(len(manifest["privacySummary"]) <= 500, "privacySummary should stay concise") - - -def check_findings(findings): - require(isinstance(findings, list), "findings.example.json must be an array") - for i, finding in enumerate(findings): - require(finding.get("severity") in SEVERITIES, f"finding {i} has unsupported severity") - for key in ["title", "summary", "evidence"]: - require(key in finding, f"finding {i} missing {key}") - require(len(finding["title"]) <= 120, f"finding {i} title is too long") - require(len(finding["summary"]) <= 500, f"finding {i} summary is too long") - require(isinstance(finding["evidence"], list) and len(finding["evidence"]) <= 8, f"finding {i} evidence must be a short array") - text = json.dumps(finding, ensure_ascii=False) - require(not SECRET_RE.search(text), f"finding {i} appears to contain a secret-like value") - for evidence in finding["evidence"]: - require(len(evidence.get("label", "")) <= 80, f"finding {i} evidence label is too long") - require(len(evidence.get("value", "")) <= 240, f"finding {i} evidence value is too long") - - -def main(): - check_manifest(load("manifest.json")) - check_findings(load("findings.example.json")) - print("ok") - - -if __name__ == "__main__": - main()