diff --git a/Flowlight/App/TrafficMonitor.swift b/Flowlight/App/TrafficMonitor.swift
index 4ade6147..47471ae7 100644
--- a/Flowlight/App/TrafficMonitor.swift
+++ b/Flowlight/App/TrafficMonitor.swift
@@ -56,6 +56,8 @@ final class TrafficMonitor: ObservableObject {
let rules = RuleStore()
/// Which tools each agent may use, which is a different question from which hosts it may reach.
let guardrails = GuardrailStore()
+ /// Advisory inspection plugins: official built-ins first, installed third-party packs later.
+ let plugins = PluginStore()
/// Channels that aren't the network: Bluetooth, USB. Both off until switched on.
let devices = DeviceStore()
/// Questions in plain language, answered from the recorded history.
@@ -157,6 +159,7 @@ final class TrafficMonitor: ObservableObject {
Task { @MainActor in self?.rules.record([event]) }
}
guardrails.attach(db: db)
+ plugins.attach(db: db)
devices.attach(db: db)
ask.attach(db: readDB)
ask.environment = { [weak self] in self?.askEnvironment() ?? .empty }
@@ -638,6 +641,10 @@ final class TrafficMonitor: ObservableObject {
Task { await localRisk.backfillRecent() }
}
+ func clearPluginFindings() {
+ plugins.clearFindings()
+ }
+
func clearLocalRiskAnalyses() {
Task { [weak self, db] in
db.async { db in
diff --git a/Flowlight/Plugins/PluginEngine.swift b/Flowlight/Plugins/PluginEngine.swift
new file mode 100644
index 00000000..688c9f44
--- /dev/null
+++ b/Flowlight/Plugins/PluginEngine.swift
@@ -0,0 +1,187 @@
+import Foundation
+
+protocol InspectionPluginRule: Sendable {
+ var manifest: PluginManifest { get }
+ func evaluate(_ context: PluginContext) -> [PluginFindingDraft]
+}
+
+struct PluginContext: Sendable {
+ var exchange: HTTPExchange
+ var requestHeaderNames: Set
+ var responseHeaderNames: Set
+ var pathWithoutQuery: String
+
+ init(exchange: HTTPExchange) {
+ self.exchange = exchange
+ requestHeaderNames = Set(exchange.requestHeaders.map { $0.name.lowercased() })
+ responseHeaderNames = Set(exchange.responseHeaders.map { $0.name.lowercased() })
+ pathWithoutQuery = exchange.path.split(separator: "?", maxSplits: 1).first.map(String.init) ?? exchange.path
+ }
+
+ var agentLabel: String { exchange.agentName ?? exchange.agent ?? exchange.appName }
+ var isLLMOrMCP: Bool { exchange.llm != nil || !exchange.toolCalls.isEmpty || !exchange.mcp.isEmpty || exchange.mcpServer != nil }
+}
+
+enum PluginEngine {
+ static let builtInRules: [any InspectionPluginRule] = [
+ LargeUploadRule(),
+ RiskyMethodRule(),
+ SensitiveHeaderRule(),
+ MCPOverHTTPRule(),
+ ProviderConnectorRule(),
+ HighRiskToolRule(),
+ ]
+
+ static var builtInManifests: [PluginManifest] { builtInRules.map(\.manifest) }
+
+ static func evaluate(_ exchange: HTTPExchange, manifests: [PluginManifest]) -> [PluginFindingDraft] {
+ 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 [] }
+ return rule.evaluate(context)
+ }
+ }
+
+ static func mergedManifests(_ stored: [PluginManifest]) -> [PluginManifest] {
+ var byID = Dictionary(uniqueKeysWithValues: stored.map { ($0.id, $0) })
+ for builtIn in builtInManifests {
+ if var existing = byID[builtIn.id] {
+ let enabled = existing.enabled
+ existing = builtIn
+ existing.enabled = enabled
+ byID[builtIn.id] = existing
+ } else {
+ byID[builtIn.id] = builtIn
+ }
+ }
+ return byID.values.sorted { $0.name < $1.name }
+ }
+}
+
+private extension InspectionPluginRule {
+ func finding(_ severity: PluginFinding.Severity, _ title: String, _ summary: String,
+ _ evidence: [PluginEvidence], suggestedGuardrail: Guardrail? = nil) -> PluginFindingDraft {
+ PluginFindingDraft(pluginID: manifest.id, pluginVersion: manifest.version, kind: manifest.kind, severity: severity,
+ title: title, summary: summary, evidence: evidence, suggestedGuardrail: suggestedGuardrail)
+ }
+}
+
+private struct LargeUploadRule: InspectionPluginRule {
+ let manifest = PluginManifest(
+ id: "traffic.large-upload", name: "Large uploads", version: "1.0.0", kind: .traffic,
+ description: L("Flags inspected requests that upload unusually large payloads."),
+ privacySummary: L("Uses request size, method, app and host metadata only."))
+
+ func evaluate(_ context: PluginContext) -> [PluginFindingDraft] {
+ guard context.exchange.requestSize >= 512 * 1024 else { return [] }
+ return [finding(.medium, L("Large upload"),
+ L("%@ sent %@ in one inspected %@ request.", context.exchange.appName, ByteCountFormatter.string(fromByteCount: Int64(context.exchange.requestSize), countStyle: .file), context.exchange.method),
+ [PluginEvidence(L("Host"), context.exchange.host), PluginEvidence(L("Method"), context.exchange.method),
+ PluginEvidence(L("Request size"), ByteCountFormatter.string(fromByteCount: Int64(context.exchange.requestSize), countStyle: .file))])]
+ }
+}
+
+private struct RiskyMethodRule: InspectionPluginRule {
+ let manifest = PluginManifest(
+ id: "traffic.risky-method", name: "Risky HTTP methods", version: "1.0.0", kind: .traffic,
+ description: L("Highlights destructive-looking HTTP requests."),
+ privacySummary: L("Uses method, redacted path shape and host metadata only."))
+
+ func evaluate(_ context: PluginContext) -> [PluginFindingDraft] {
+ let method = context.exchange.method.uppercased()
+ guard ["DELETE", "PATCH", "PUT"].contains(method) else { return [] }
+ let path = context.pathWithoutQuery.lowercased()
+ let interesting = ["/admin", "/delete", "/destroy", "/token", "/key", "/secret", "/user"].contains { path.contains($0) }
+ guard interesting || method == "DELETE" else { return [] }
+ return [finding(method == "DELETE" ? .medium : .low, L("Destructive-looking request"),
+ L("%@ made a %@ request to a sensitive-looking path on %@.", context.exchange.appName, method, context.exchange.host),
+ [PluginEvidence(L("Host"), context.exchange.host), PluginEvidence(L("Method"), method),
+ PluginEvidence(L("Path"), context.pathWithoutQuery, limit: 160)])]
+ }
+}
+
+private struct SensitiveHeaderRule: InspectionPluginRule {
+ let manifest = PluginManifest(
+ id: "traffic.sensitive-headers", name: "Sensitive header names", version: "1.0.0", kind: .traffic,
+ description: L("Notes requests carrying credential-like header names."),
+ privacySummary: L("Reads header names only; values are never exposed."))
+
+ func evaluate(_ context: PluginContext) -> [PluginFindingDraft] {
+ let sensitive = context.requestHeaderNames.filter { name in
+ name == "authorization" || name == "cookie" || name.contains("api-key") || name.contains("token") || name.contains("secret")
+ }.sorted()
+ guard !sensitive.isEmpty else { return [] }
+ return [finding(.info, L("Credential-like headers present"),
+ L("This request included credential-like header names. Values remain redacted."),
+ [PluginEvidence(L("Header names"), sensitive.joined(separator: ", "))])]
+ }
+}
+
+private struct MCPOverHTTPRule: InspectionPluginRule {
+ let manifest = PluginManifest(
+ id: "llm-mcp.http-mcp", name: "HTTP MCP activity", version: "1.0.0", kind: .llmMCP,
+ description: L("Surfaces direct MCP activity observed over inspected HTTP."),
+ privacySummary: L("Uses MCP method, server and tool names only."))
+
+ func evaluate(_ context: PluginContext) -> [PluginFindingDraft] {
+ guard !context.exchange.mcp.isEmpty else { return [] }
+ let methods = Set(context.exchange.mcp.map(\.method)).sorted().joined(separator: ", ")
+ return [finding(.info, L("MCP over HTTP observed"),
+ L("%@ used MCP over inspected HTTP with %@.", context.agentLabel, context.exchange.host),
+ [PluginEvidence(L("Methods"), methods), PluginEvidence(L("Host"), context.exchange.host)])]
+ }
+}
+
+private struct ProviderConnectorRule: InspectionPluginRule {
+ let manifest = PluginManifest(
+ id: "llm-mcp.provider-connectors", name: "Provider-run MCP connectors", version: "1.0.0", kind: .llmMCP,
+ description: L("Explains MCP connectors that run on the model provider side."),
+ privacySummary: L("Uses connector labels, provider names and approval metadata only."))
+
+ func evaluate(_ context: PluginContext) -> [PluginFindingDraft] {
+ guard let llm = context.exchange.llm, !llm.connectors.isEmpty else { return [] }
+ return llm.connectors.map { connector in
+ var evidence = [PluginEvidence(L("Provider"), connector.provider), PluginEvidence(L("Connector"), connector.label)]
+ if let approval = connector.approval { evidence.append(PluginEvidence(L("Approval"), approval)) }
+ if connector.authorized { evidence.append(PluginEvidence(L("Authorization"), L("configured"))) }
+ let severity: PluginFinding.Severity = connector.authorized ? .medium : .low
+ return finding(severity, L("Provider-run MCP connector"),
+ L("%@ offered %@ through %@. Its network calls run outside this Mac, so local network rules may not see them.",
+ context.agentLabel, connector.label, connector.provider), evidence)
+ }
+ }
+}
+
+private struct HighRiskToolRule: InspectionPluginRule {
+ let manifest = PluginManifest(
+ id: "llm-mcp.high-risk-tools", name: "High-risk tools", version: "1.0.0", kind: .llmMCP,
+ description: L("Suggests guardrails for declared or used tools with write, shell or delete capability."),
+ privacySummary: L("Uses tool and MCP server names only, not tool arguments."), capabilities: [.annotate, .suggestGuardrail])
+
+ private let patterns = ["bash", "shell", "exec", "terminal", "write", "edit", "delete", "remove", "create", "update", "patch", "apply"]
+
+ func evaluate(_ context: PluginContext) -> [PluginFindingDraft] {
+ var tools: [(name: String, server: String?)] = []
+ if let llm = context.exchange.llm {
+ tools += llm.declaredTools.compactMap { tool in
+ guard tool.kind == .function else { return nil }
+ return (tool.name, tool.server)
+ }
+ }
+ tools += context.exchange.toolCalls.map { ($0.name, $0.mcpServer) }
+ var seen = Set()
+ return tools.compactMap { tool in
+ let key = [tool.server ?? "", tool.name].joined(separator: "/")
+ guard seen.insert(key).inserted else { return nil }
+ let lower = tool.name.lowercased()
+ guard patterns.contains(where: { lower.contains($0) }) else { return nil }
+ var guardrail = Guardrail(agent: context.exchange.agent ?? "", server: tool.server ?? "", tool: tool.name, origin: .observed)
+ guardrail.name = L("Review %@", tool.server.map { "\($0) › \(tool.name)" } ?? tool.name)
+ return finding(.medium, L("High-risk tool available"),
+ L("%@ declared or used %@. Add a guardrail if this tool should be refused.", context.agentLabel, tool.server.map { "\($0) › \(tool.name)" } ?? tool.name),
+ [PluginEvidence(L("Tool"), tool.name), PluginEvidence(L("Server"), tool.server ?? L("agent tool"))],
+ suggestedGuardrail: guardrail)
+ }
+ }
+}
diff --git a/Flowlight/Plugins/PluginModels.swift b/Flowlight/Plugins/PluginModels.swift
new file mode 100644
index 00000000..72fea8cc
--- /dev/null
+++ b/Flowlight/Plugins/PluginModels.swift
@@ -0,0 +1,140 @@
+import Foundation
+
+/// Advisory plugin definitions and findings. Plugins do not alter traffic; they add derived context to inspected exchanges.
+struct PluginManifest: Codable, Identifiable, Equatable, Sendable {
+ var id: String
+ var name: String
+ var version: String
+ var kind: Kind
+ var enabled: Bool = true
+ var source: Source = .builtIn
+ var publisher: Publisher = .official
+ var guardrailProvider: GuardrailProvider = .flowlight
+ var description: String
+ var ruleVersion: Int = 1
+ var privacySummary: String
+ var capabilities: [Capability] = [.annotate]
+
+ enum Kind: String, Codable, Sendable { case traffic, llmMCP }
+ enum Source: String, Codable, Sendable { case builtIn, installed }
+ enum Publisher: String, Codable, Sendable { case official, thirdParty }
+ enum GuardrailProvider: String, Codable, Sendable, CaseIterable {
+ case flowlight, presidio, bedrock, lakera, aporia, noma, prismaAIRS, custom
+
+ var title: String {
+ switch self {
+ case .flowlight: return "Flowlight"
+ case .presidio: return "Presidio"
+ case .bedrock: return "Bedrock"
+ case .lakera: return "Lakera"
+ case .aporia: return "Aporia"
+ case .noma: return "Noma"
+ case .prismaAIRS: return "PANW Prisma AIRS"
+ case .custom: return "Custom"
+ }
+ }
+ }
+ enum Capability: String, Codable, Sendable { case annotate, suggestGuardrail }
+
+ 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]) {
+ self.id = id
+ self.name = name
+ self.version = version
+ self.kind = kind
+ self.enabled = enabled
+ self.source = source
+ self.publisher = publisher
+ self.guardrailProvider = guardrailProvider
+ self.description = description
+ self.ruleVersion = ruleVersion
+ self.privacySummary = privacySummary
+ self.capabilities = capabilities
+ }
+
+ private enum CodingKeys: String, CodingKey {
+ case id, name, version, kind, enabled, source, publisher, guardrailProvider, description, ruleVersion, privacySummary, capabilities
+ }
+
+ init(from decoder: Decoder) throws {
+ let values = try decoder.container(keyedBy: CodingKeys.self)
+ id = try values.decode(String.self, forKey: .id)
+ name = try values.decode(String.self, forKey: .name)
+ version = try values.decode(String.self, forKey: .version)
+ kind = try values.decode(Kind.self, forKey: .kind)
+ enabled = try values.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
+ source = try values.decodeIfPresent(Source.self, forKey: .source) ?? .builtIn
+ publisher = try values.decodeIfPresent(Publisher.self, forKey: .publisher) ?? .official
+ guardrailProvider = try values.decodeIfPresent(GuardrailProvider.self, forKey: .guardrailProvider) ?? .flowlight
+ description = try values.decode(String.self, forKey: .description)
+ 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]
+ }
+}
+
+struct PluginEvidence: Codable, Equatable, Sendable {
+ var label: String
+ var value: String
+
+ init(_ label: String, _ value: String, limit: Int = 240) {
+ self.label = String(label.prefix(80))
+ self.value = String(value.prefix(limit))
+ }
+}
+
+struct PluginFinding: Codable, Identifiable, Equatable, Sendable {
+ var id: String
+ var exchangeID: Int64
+ var pluginID: String
+ var pluginVersion: String
+ var kind: PluginManifest.Kind
+ var severity: Severity
+ var title: String
+ var summary: String
+ var evidence: [PluginEvidence]
+ var suggestedGuardrail: Guardrail?
+ var createdAt: Date
+
+ enum Severity: String, Codable, Sendable, CaseIterable { case info, low, medium, high }
+}
+
+struct PluginFindingDraft: Equatable, Sendable {
+ var pluginID: String
+ var pluginVersion: String
+ var kind: PluginManifest.Kind
+ var severity: PluginFinding.Severity
+ var title: String
+ var summary: String
+ var evidence: [PluginEvidence]
+ var suggestedGuardrail: Guardrail?
+
+ func materialize(exchangeID: Int64, createdAt: Date = Date()) -> PluginFinding {
+ let stable = [String(exchangeID), pluginID, title].joined(separator: "|")
+ return PluginFinding(id: StablePluginID.make(stable), exchangeID: exchangeID, pluginID: pluginID, pluginVersion: pluginVersion,
+ kind: kind, severity: severity, title: title, summary: summary, evidence: evidence,
+ suggestedGuardrail: suggestedGuardrail, createdAt: createdAt)
+ }
+}
+
+extension PluginManifest.Kind {
+ var title: String {
+ switch self {
+ case .traffic: return L("Traffic")
+ case .llmMCP: return L("LLM/MCP")
+ }
+ }
+}
+
+private enum StablePluginID {
+ /// Deterministic enough for de-duplicating plugin findings without pulling in a hashing dependency.
+ static func make(_ text: String) -> String {
+ var hash: UInt64 = 0xcbf29ce484222325
+ for byte in text.utf8 {
+ hash ^= UInt64(byte)
+ hash &*= 0x100000001b3
+ }
+ return String(format: "%016llx", hash)
+ }
+}
diff --git a/Flowlight/Plugins/PluginStore.swift b/Flowlight/Plugins/PluginStore.swift
new file mode 100644
index 00000000..94b69aa8
--- /dev/null
+++ b/Flowlight/Plugins/PluginStore.swift
@@ -0,0 +1,48 @@
+import Foundation
+
+@MainActor
+final class PluginStore: ObservableObject {
+ @Published private(set) var manifests: [PluginManifest] = []
+ @Published private(set) var version = 0
+
+ private weak var db: TrafficDatabase?
+
+ func attach(db: TrafficDatabase) {
+ self.db = db
+ reload()
+ }
+
+ func reload() {
+ guard let db else { return }
+ db.async { [weak self] db in
+ let merged = PluginEngine.mergedManifests((try? db.loadPluginManifests()) ?? [])
+ for manifest in merged where manifest.source == .builtIn {
+ try? db.savePluginManifest(manifest)
+ }
+ Task { @MainActor in
+ self?.manifests = merged
+ self?.version += 1
+ }
+ }
+ }
+
+ func setEnabled(_ manifest: PluginManifest, _ enabled: Bool) {
+ guard var updated = manifests.first(where: { $0.id == manifest.id }) else { return }
+ updated.enabled = enabled
+ if let index = manifests.firstIndex(where: { $0.id == updated.id }) {
+ manifests[index] = updated
+ }
+ db?.async { try $0.setPluginEnabled(id: updated.id, enabled: enabled) }
+ version += 1
+ }
+
+ func clearFindings() {
+ db?.async { [weak self] db in
+ try db.clearPluginFindings()
+ Task { @MainActor in self?.version += 1 }
+ }
+ }
+
+ var officialPlugins: [PluginManifest] { manifests.filter { $0.publisher == .official } }
+ var thirdPartyPlugins: [PluginManifest] { manifests.filter { $0.publisher == .thirdParty } }
+}
diff --git a/Flowlight/Storage/TrafficDatabase.swift b/Flowlight/Storage/TrafficDatabase.swift
index 6c9ab6f9..9cdd715e 100644
--- a/Flowlight/Storage/TrafficDatabase.swift
+++ b/Flowlight/Storage/TrafficDatabase.swift
@@ -384,6 +384,28 @@ final class TrafficDatabase: @unchecked Sendable {
);
CREATE INDEX IF NOT EXISTS local_risk_assessments_state_created ON local_risk_assessments (state, created_at);
CREATE INDEX IF NOT EXISTS local_risk_assessments_severity_created ON local_risk_assessments (severity, created_at);
+ CREATE TABLE IF NOT EXISTS plugins (
+ id TEXT PRIMARY KEY,
+ manifest TEXT NOT NULL,
+ enabled INTEGER NOT NULL,
+ updated INTEGER NOT NULL
+ );
+ CREATE TABLE IF NOT EXISTS plugin_findings (
+ id TEXT PRIMARY KEY,
+ exchange_id INTEGER NOT NULL,
+ plugin_id TEXT NOT NULL,
+ plugin_version TEXT NOT NULL,
+ kind TEXT NOT NULL,
+ severity TEXT NOT NULL,
+ title TEXT NOT NULL,
+ summary TEXT NOT NULL,
+ evidence TEXT NOT NULL,
+ suggested_guardrail TEXT NOT NULL DEFAULT '',
+ created_at REAL NOT NULL,
+ UNIQUE(exchange_id, plugin_id, title)
+ );
+ CREATE INDEX IF NOT EXISTS plugin_findings_exchange ON plugin_findings (exchange_id);
+ CREATE INDEX IF NOT EXISTS plugin_findings_plugin_created ON plugin_findings (plugin_id, created_at);
""")
// Added with blocking: the destination a refused connection was headed for, so its alert can offer to
// allow it without parsing the sentence back out of `detail`.
@@ -974,6 +996,86 @@ final class TrafficDatabase: @unchecked Sendable {
try conn.run("DELETE FROM guardrails WHERE id = ?", [.text(id.uuidString)])
}
+ // MARK: Plugins
+
+ func loadPluginManifests() throws -> [PluginManifest] {
+ let rows = try conn.query("SELECT manifest, enabled FROM plugins ORDER BY updated") { ($0.text(0), $0.int(1) != 0) }
+ return rows.compactMap { text, enabled in
+ guard var manifest = try? JSONDecoder().decode(PluginManifest.self, from: Data(text.utf8)) else { return nil }
+ manifest.enabled = enabled
+ return manifest
+ }
+ }
+
+ func savePluginManifest(_ manifest: PluginManifest) throws {
+ let json = String(decoding: try JSONEncoder().encode(manifest), as: UTF8.self)
+ try conn.run("""
+ INSERT INTO plugins (id, manifest, enabled, updated) VALUES (?,?,?,?)
+ ON CONFLICT(id) DO UPDATE SET manifest = excluded.manifest, enabled = excluded.enabled, updated = excluded.updated
+ """, [.text(manifest.id), .text(json), .int(manifest.enabled ? 1 : 0), .int(Int64(Date().timeIntervalSince1970))])
+ }
+
+ func setPluginEnabled(id: String, enabled: Bool) throws {
+ if var manifest = try loadPluginManifests().first(where: { $0.id == id }) {
+ manifest.enabled = enabled
+ try savePluginManifest(manifest)
+ } else if var manifest = PluginEngine.builtInManifests.first(where: { $0.id == id }) {
+ manifest.enabled = enabled
+ try savePluginManifest(manifest)
+ }
+ }
+
+ func insertPluginFindings(_ findings: [PluginFindingDraft], forExchangeID exchangeID: Int64) throws {
+ guard !findings.isEmpty else { return }
+ let encoder = JSONEncoder()
+ for draft in findings {
+ let finding = draft.materialize(exchangeID: exchangeID)
+ let evidence = String(decoding: try encoder.encode(finding.evidence), as: UTF8.self)
+ let guardrail: String
+ if let suggested = finding.suggestedGuardrail {
+ guardrail = String(decoding: try encoder.encode(suggested), as: UTF8.self)
+ } else {
+ guardrail = ""
+ }
+ try conn.run("""
+ INSERT OR IGNORE INTO plugin_findings (id, exchange_id, plugin_id, plugin_version, kind, severity, title, summary, evidence, suggested_guardrail, created_at)
+ VALUES (?,?,?,?,?,?,?,?,?,?,?)
+ """, [.text(finding.id), .int(exchangeID), .text(finding.pluginID), .text(finding.pluginVersion), .text(finding.kind.rawValue),
+ .text(finding.severity.rawValue), .text(finding.title), .text(String(finding.summary.prefix(500))), .text(evidence),
+ .text(guardrail), .double(finding.createdAt.timeIntervalSince1970)])
+ }
+ }
+
+ func pluginFindings(exchangeIDs: [Int64], visibleOnly: Bool = true) throws -> [Int64: [PluginFinding]] {
+ guard !exchangeIDs.isEmpty else { return [:] }
+ let manifests = try loadPluginManifests()
+ let enabled = Dictionary(uniqueKeysWithValues: PluginEngine.mergedManifests(manifests).map { ($0.id, $0.enabled) })
+ let marks = Array(repeating: "?", count: exchangeIDs.count).joined(separator: ",")
+ let values = exchangeIDs.map(SQLValue.int)
+ let rows = try conn.query("""
+ SELECT id, exchange_id, plugin_id, plugin_version, kind, severity, title, summary, evidence, suggested_guardrail, created_at
+ FROM plugin_findings WHERE exchange_id IN (\(marks)) ORDER BY created_at ASC, title ASC
+ """, values) { row in decodePluginFinding(row) }
+ let visible = visibleOnly ? rows.filter { enabled[$0.pluginID] ?? true } : rows
+ return Dictionary(grouping: visible, by: \.exchangeID)
+ }
+
+ func clearPluginFindings() throws {
+ try conn.run("DELETE FROM plugin_findings")
+ }
+
+ private func decodePluginFinding(_ row: SQLRow) -> PluginFinding {
+ let decoder = JSONDecoder()
+ let evidence = (try? decoder.decode([PluginEvidence].self, from: Data(row.text(8).utf8))) ?? []
+ let guardrailText = row.text(9)
+ let guardrail = guardrailText.isEmpty ? nil : try? decoder.decode(Guardrail.self, from: Data(guardrailText.utf8))
+ return PluginFinding(id: row.text(0), exchangeID: row.int(1), pluginID: row.text(2), pluginVersion: row.text(3),
+ kind: PluginManifest.Kind(rawValue: row.text(4)) ?? .traffic,
+ severity: PluginFinding.Severity(rawValue: row.text(5)) ?? .info,
+ title: row.text(6), summary: row.text(7), evidence: evidence, suggestedGuardrail: guardrail,
+ createdAt: Date(timeIntervalSince1970: row.double(10)))
+ }
+
// MARK: Rule simulation
/// Recorded traffic in the shape the rule engine judges, for answering "what would this rule have done".
@@ -1002,9 +1104,10 @@ final class TrafficDatabase: @unchecked Sendable {
// MARK: HTTPS inspection
- /// Inserts raw capture and, when deterministic triage finds enough independent evidence, creates its separate
- /// derived job in the same transaction. This returns before any model work begins.
+ /// Inserts raw capture and, when deterministic triage or plugin rules find bounded evidence, creates separate
+ /// derived rows in the same transaction. This returns before any model work begins.
func insertExchange(_ e: HTTPExchange, enqueueLocalRisk: Bool = false) throws -> Int64 {
+ var insertedID: Int64 = 0
try conn.transaction {
let encoder = JSONEncoder()
func json(_ v: T) -> String { (try? encoder.encode(v)).map { String(decoding: $0, as: UTF8.self) } ?? "[]" }
@@ -1021,16 +1124,23 @@ final class TrafficDatabase: @unchecked Sendable {
.text(e.agentName ?? ""), .text(e.mcpServer ?? ""), .text(e.toolCalls.isEmpty ? "" : json(e.toolCalls)), .text(e.note ?? ""),
.text(e.toolResults.isEmpty ? "" : json(e.toolResults)), .text(e.mcp.isEmpty ? "" : json(e.mcp)),
.text(e.llm.map(json) ?? ""), .text(e.mockRule ?? ""), .text(e.guardrail ?? ""), .text(e.responseTransform ?? "")])
- let id = try conn.query("SELECT last_insert_rowid()", map: { $0.int(0) }).first ?? 0
- if enqueueLocalRisk, LocalRiskSettings.enabled {
- var stored = e
- stored.id = id
- if let candidate = LocalRiskTriage.candidate(for: stored) {
- try enqueueLocalRiskCandidate(candidate)
+ insertedID = try conn.query("SELECT last_insert_rowid()", map: { $0.int(0) }).first ?? 0
+ var stored = e
+ stored.id = insertedID
+ if enqueueLocalRisk, LocalRiskSettings.enabled, let candidate = LocalRiskTriage.candidate(for: stored) {
+ try enqueueLocalRiskCandidate(candidate)
+ }
+ do {
+ let manifests = PluginEngine.mergedManifests(try loadPluginManifests())
+ for manifest in manifests where PluginEngine.builtInManifests.contains(where: { $0.id == manifest.id }) {
+ try savePluginManifest(manifest)
}
+ try insertPluginFindings(PluginEngine.evaluate(stored, manifests: manifests), forExchangeID: insertedID)
+ } catch {
+ appLog.error("plugin evaluation skipped: \(String(describing: error), privacy: .public)")
}
}
- return try conn.query("SELECT last_insert_rowid()", map: { $0.int(0) }).first ?? 0
+ return insertedID
}
private func enqueueLocalRiskCandidate(_ candidate: RiskCandidate) throws {
@@ -1267,6 +1377,8 @@ final class TrafficDatabase: @unchecked Sendable {
try conn.transaction {
try conn.run("DELETE FROM local_risk_assessments WHERE exchange_id IN (SELECT id FROM http_exchanges WHERE ts < ?)",
[.double(cutoff.timeIntervalSince1970)])
+ try conn.run("DELETE FROM plugin_findings WHERE exchange_id IN (SELECT id FROM http_exchanges WHERE ts < ?)",
+ [.double(cutoff.timeIntervalSince1970)])
try conn.run("DELETE FROM http_exchanges WHERE ts < ?", [.double(cutoff.timeIntervalSince1970)])
}
}
@@ -1274,6 +1386,7 @@ final class TrafficDatabase: @unchecked Sendable {
func deleteAllExchanges() throws {
try conn.transaction {
try conn.run("DELETE FROM local_risk_assessments")
+ try conn.run("DELETE FROM plugin_findings")
try conn.run("DELETE FROM http_exchanges")
}
}
@@ -1380,7 +1493,7 @@ final class TrafficDatabase: @unchecked Sendable {
func clearAll() throws {
try conn.transaction {
- for t in Self.tables + ["rollup_state", "seen_destinations", "seen_ports", "apps", "baselines", "alerts", "http_exchanges", "local_risk_assessments"] {
+ for t in Self.tables + ["rollup_state", "seen_destinations", "seen_ports", "apps", "baselines", "alerts", "http_exchanges", "local_risk_assessments", "plugin_findings"] {
try conn.run("DELETE FROM \(t)")
}
}
diff --git a/Flowlight/UI/InspectView.swift b/Flowlight/UI/InspectView.swift
index dcd53e7b..81b83a2b 100644
--- a/Flowlight/UI/InspectView.swift
+++ b/Flowlight/UI/InspectView.swift
@@ -25,6 +25,7 @@ private struct InspectContent: View {
@State private var scopeHost: String?
@State private var potentialHarmOnly = false
@State private var assessments: [Int64: RiskAssessment] = [:]
+ @State private var pluginFindings: [Int64: [PluginFinding]] = [:]
@State private var window: AgentWindow = .day
@State private var showSetup = false
/// A rule plus safe, temporary captured context waiting in the mock editor.
@@ -103,8 +104,9 @@ private struct InspectContent: View {
.sheet(item: $responseTransformDraft) { draft in
ResponseTransformRuleEditor(rule: draft.rule, isNew: true, draft: draft) { inspection.responseTransformRules.append($0) }
}
- .task(id: LoadKey(version: monitor.inspectionVersion, localRiskVersion: monitor.localRiskVersion, search: search, window: window,
- enabled: inspection.enabled, potentialHarmOnly: potentialHarmOnly, focus: focus.scope, app: scopeApp?.id, host: scopeHost)) {
+ .task(id: LoadKey(version: monitor.inspectionVersion, localRiskVersion: monitor.localRiskVersion, pluginVersion: monitor.plugins.version,
+ search: search, window: window, enabled: inspection.enabled,
+ potentialHarmOnly: potentialHarmOnly, focus: focus.scope, app: scopeApp?.id, host: scopeHost)) {
// Coalesce bursts of new exchanges.
try? await Task.sleep(for: .milliseconds(300))
await load()
@@ -112,7 +114,7 @@ private struct InspectContent: View {
}
private struct LoadKey: Equatable {
- var version: Int; var localRiskVersion: Int; var search: String; var window: AgentWindow; var enabled: Bool
+ var version: Int; var localRiskVersion: Int; var pluginVersion: Int; var search: String; var window: AgentWindow; var enabled: Bool
var potentialHarmOnly: Bool; var focus: FocusScope; var app: String?; var host: String?
}
@@ -150,10 +152,15 @@ private struct InspectContent: View {
let loaded = (try? await monitor.read {
try $0.exchanges(since: since, search: term, focus: scope, app: app, host: host)
}) ?? []
+ let ids = loaded.compactMap(\.id)
let risk = (try? await monitor.read {
- try $0.localRiskAssessments(exchangeIDs: loaded.compactMap(\.id))
+ try $0.localRiskAssessments(exchangeIDs: ids)
+ }) ?? [:]
+ let findings = (try? await monitor.read {
+ try $0.pluginFindings(exchangeIDs: ids)
}) ?? [:]
assessments = risk
+ pluginFindings = findings
exchanges = potentialHarmOnly ? loaded.filter { $0.id.flatMap { risk[$0] }?.isPotentialHarm == true } : loaded
// `-FLInspectSelect paste.example`, so the published screenshot always shows the same request rather
// than whichever one a click happened to land on.
@@ -285,6 +292,10 @@ private struct InspectContent: View {
RiskBadge(assessment: e.id.flatMap { assessments[$0] })
}
.width(min: 76, ideal: 96)
+ TableColumn(L("Plugins")) { e in
+ PluginFindingBadge(findings: e.id.flatMap { pluginFindings[$0] } ?? [])
+ }
+ .width(min: 58, ideal: 68)
TableColumn(L("Status")) { e in
Text(e.status.map(String.init) ?? "–").monospacedDigit()
.foregroundStyle((e.status ?? 0) >= 400 ? FL.critical : .primary)
@@ -327,8 +338,10 @@ private struct InspectContent: View {
@ViewBuilder private var detail: some View {
if let selected = exchanges.first(where: { $0.id == selection }) {
- ExchangeDetail(exchange: selected, assessment: selected.id.flatMap { assessments[$0] }, cause: selected.id.flatMap { links[$0] },
- results: results, highlight: search, mockThis: DemoData.isEnabled ? nil : { prepareMock(from: selected) },
+ ExchangeDetail(exchange: selected, assessment: selected.id.flatMap { assessments[$0] },
+ pluginFindings: selected.id.flatMap { pluginFindings[$0] } ?? [],
+ cause: selected.id.flatMap { links[$0] }, results: results, highlight: search,
+ mockThis: DemoData.isEnabled ? nil : { prepareMock(from: selected) },
modifyResponse: canModifyResponse(selected) ? { prepareResponseTransform(from: selected) } : nil)
.id(selected.id)
} else {
@@ -533,11 +546,105 @@ private struct InspectionSetup: View {
}
}
+private struct PluginFindingBadge: View {
+ var findings: [PluginFinding]
+
+ var body: some View {
+ if findings.isEmpty {
+ Text(L("None")).font(.caption2).foregroundStyle(.tertiary)
+ } else {
+ Label("\(findings.count)", systemImage: topSeverity.symbol)
+ .labelStyle(.titleAndIcon)
+ .font(.caption2.bold())
+ .foregroundStyle(topSeverity.color)
+ .help(findings.map(\.title).joined(separator: " · "))
+ }
+ }
+
+ private var topSeverity: PluginFinding.Severity {
+ findings.map(\.severity).max(by: { $0.rank < $1.rank }) ?? .info
+ }
+}
+
+private struct PluginFindingsCard: View {
+ var findings: [PluginFinding]
+ var addGuardrail: (Guardrail) -> Void
+
+ var body: some View {
+ if !findings.isEmpty {
+ GroupBox(L("Plugins")) {
+ VStack(alignment: .leading, spacing: 8) {
+ ForEach(findings) { finding in
+ VStack(alignment: .leading, spacing: 4) {
+ HStack(spacing: 6) {
+ Label(finding.title, systemImage: finding.severity.symbol)
+ .font(.callout.bold()).foregroundStyle(finding.severity.color)
+ Spacer()
+ Text(finding.kind.title).font(.caption2.bold()).foregroundStyle(.secondary)
+ }
+ Text(finding.summary).font(.caption).fixedSize(horizontal: false, vertical: true)
+ if !finding.evidence.isEmpty {
+ VStack(alignment: .leading, spacing: 2) {
+ ForEach(Array(finding.evidence.enumerated()), id: \.offset) { _, evidence in
+ Text("\(evidence.label): \(evidence.value)")
+ .font(.caption2.monospaced()).foregroundStyle(.secondary).lineLimit(3)
+ }
+ }
+ }
+ HStack(spacing: 6) {
+ Text(L("%@ · %@", finding.pluginID, finding.pluginVersion))
+ .font(.caption2).foregroundStyle(.tertiary).lineLimit(1)
+ if let guardrail = finding.suggestedGuardrail {
+ Spacer()
+ Button(L("Add Guardrail")) { addGuardrail(guardrail) }
+ .buttonStyle(.link).font(.caption)
+ .help(L("Save this as a normal Flowlight guardrail"))
+ }
+ }
+ }
+ .padding(.vertical, 2)
+ if finding.id != findings.last?.id { Divider() }
+ }
+ Text(L("Plugin findings are advisory. They did not block or change this request."))
+ .font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(4)
+ }
+ }
+ }
+}
+
+private extension PluginFinding.Severity {
+ var rank: Int {
+ switch self { case .info: return 0; case .low: return 1; case .medium: return 2; case .high: return 3 }
+ }
+
+ var symbol: String {
+ switch self {
+ case .info: return "info.circle"
+ case .low: return "exclamationmark.circle"
+ case .medium: return "exclamationmark.triangle.fill"
+ case .high: return "xmark.octagon.fill"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .info: return .secondary
+ case .low: return FL.warning
+ case .medium: return .orange
+ case .high: return FL.critical
+ }
+ }
+}
+
/// One request and response: tool calls first, then headers and bodies.
private struct ExchangeDetail: View {
@EnvironmentObject var monitor: TrafficMonitor
let exchange: HTTPExchange
var assessment: RiskAssessment?
+ var pluginFindings: [PluginFinding] = []
var cause: ToolCallLinks.Link?
var results: [String: ToolResult] = [:]
/// The toolbar's search term, so matches inside a body are marked where they appear.
@@ -570,6 +677,7 @@ private struct ExchangeDetail: View {
if LocalRiskSettings.isActive {
PotentialHarmCard(assessment: assessment)
}
+ PluginFindingsCard(findings: pluginFindings) { monitor.guardrails.save($0) }
if let note = exchange.note {
Label(note, systemImage: "lock").foregroundStyle(FL.warning).fixedSize(horizontal: false, vertical: true)
}
diff --git a/Flowlight/UI/PluginSettingsView.swift b/Flowlight/UI/PluginSettingsView.swift
new file mode 100644
index 00000000..bd4c7380
--- /dev/null
+++ b/Flowlight/UI/PluginSettingsView.swift
@@ -0,0 +1,88 @@
+import SwiftUI
+
+struct PluginSettingsView: View {
+ @EnvironmentObject var monitor: TrafficMonitor
+ @State private var showClearConfirmation = false
+
+ 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)
+ }
+ 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"))
+ }
+ }
+ .formStyle(.grouped)
+ .frame(height: 520)
+ .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."))
+ }
+ }
+
+ private func pluginSection(_ title: String, plugins: [PluginManifest], empty: String) -> some View {
+ Section(title) {
+ if plugins.isEmpty {
+ Text(empty).font(.caption).foregroundStyle(.secondary)
+ } else {
+ ForEach(plugins) { plugin in
+ PluginSettingsRow(plugin: plugin) { enabled in
+ monitor.plugins.setEnabled(plugin, enabled)
+ }
+ }
+ }
+ }
+ }
+}
+
+private struct PluginSettingsRow: View {
+ var plugin: PluginManifest
+ var setEnabled: (Bool) -> 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")) }
+ }
+ 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)
+ }
+}
+
+private struct PluginPill: View {
+ var text: String
+ init(_ text: String) { self.text = text }
+
+ var body: some View {
+ Text(text)
+ .font(.caption2.bold())
+ .padding(.horizontal, 6).padding(.vertical, 2)
+ .background(.quaternary.opacity(0.7), in: Capsule())
+ }
+}
+
+private extension PluginManifest.Publisher {
+ var title: String {
+ switch self {
+ case .official: return L("Official")
+ case .thirdParty: return L("Third-party")
+ }
+ }
+}
diff --git a/Flowlight/UI/SettingsView.swift b/Flowlight/UI/SettingsView.swift
index 0c1db697..ba338904 100644
--- a/Flowlight/UI/SettingsView.swift
+++ b/Flowlight/UI/SettingsView.swift
@@ -108,6 +108,9 @@ struct SettingsView: View {
.frame(height: 150)
.tabItem { Label(L("Storage"), systemImage: "internaldrive") }
+ PluginSettingsView()
+ .tabItem { Label(L("Plugins"), systemImage: "puzzlepiece.extension") }
+
ExportSettingsTab()
.tabItem { Label(L("Export"), systemImage: "arrow.up.forward.square") }
}
diff --git a/FlowlightTests/PluginTests.swift b/FlowlightTests/PluginTests.swift
new file mode 100644
index 00000000..38369643
--- /dev/null
+++ b/FlowlightTests/PluginTests.swift
@@ -0,0 +1,91 @@
+import XCTest
+@testable import Flowlight
+
+final class PluginTests: XCTestCase {
+ private var url: URL!
+
+ override func setUp() {
+ super.setUp()
+ url = FileManager.default.temporaryDirectory.appendingPathComponent("plugins-\(UUID()).sqlite")
+ }
+
+ override func tearDown() {
+ try? FileManager.default.removeItem(at: url)
+ super.tearDown()
+ }
+
+ private func database() throws -> TrafficDatabase { try TrafficDatabase(url: url) }
+
+ private func exchange(method: String = "DELETE", path: String = "/admin/delete?token=secret", requestSize: Int = 700_000,
+ headers: [HTTPHeader] = [HTTPHeader(name: "Authorization", value: "Bearer secret-token"),
+ HTTPHeader(name: "X-API-Key", value: "secret-key")],
+ toolCalls: [ToolCall] = [], llm: LLMFacts? = nil, mcp: [MCPActivity] = []) -> HTTPExchange {
+ HTTPExchange(id: nil, started: Date(), duration: 0.2, scheme: "https", host: "api.example.test", port: 443,
+ method: method, path: path, status: 200, requestHeaders: headers,
+ requestBody: Data("{\"secret\":\"do-not-leak\"}".utf8), requestSize: requestSize, requestTruncated: false,
+ responseHeaders: [], responseBody: Data(), responseSize: 0, responseTruncated: false, contentType: "application/json",
+ pid: 1, bundleID: "agent", appName: "Agent", agent: "agent", agentName: "Agent", mcpServer: nil,
+ toolCalls: toolCalls, toolResults: [], mcp: mcp, llm: llm, note: nil, mockRule: nil, guardrail: nil)
+ }
+
+ func testBuiltInTrafficPluginsProduceBoundedAdvisoryFindings() {
+ let findings = PluginEngine.evaluate(exchange(), manifests: PluginEngine.builtInManifests)
+ XCTAssertTrue(findings.contains { $0.pluginID == "traffic.large-upload" })
+ XCTAssertTrue(findings.contains { $0.pluginID == "traffic.risky-method" })
+ XCTAssertTrue(findings.contains { $0.pluginID == "traffic.sensitive-headers" })
+ let allText = findings.flatMap { [$0.title, $0.summary] + $0.evidence.map(\.value) }.joined(separator: "\n")
+ XCTAssertFalse(allText.contains("secret-token"))
+ XCTAssertFalse(allText.contains("secret-key"))
+ XCTAssertFalse(allText.contains("do-not-leak"))
+ }
+
+ func testHighRiskToolPluginSuggestsARealGuardrail() throws {
+ let llm = LLMFacts(provider: .anthropic, model: "claude-test", declaredTools: [DeclaredTool(name: "Bash", kind: .function)],
+ connectors: [], usage: nil, stopReason: nil, errorType: nil)
+ let findings = PluginEngine.evaluate(exchange(requestSize: 10, headers: [], llm: llm), manifests: PluginEngine.builtInManifests)
+ let finding = try XCTUnwrap(findings.first { $0.pluginID == "llm-mcp.high-risk-tools" })
+ XCTAssertEqual(finding.kind, .llmMCP)
+ let guardrail = try XCTUnwrap(finding.suggestedGuardrail)
+ XCTAssertEqual(guardrail.origin, .observed)
+ XCTAssertEqual(guardrail.tool, "Bash")
+ XCTAssertTrue(guardrail.isComplete)
+ }
+
+ func testDisabledPluginProducesNoNewFinding() throws {
+ let manifests = PluginEngine.builtInManifests.map { manifest -> PluginManifest in
+ var copy = manifest
+ if copy.id == "traffic.large-upload" { copy.enabled = false }
+ return copy
+ }
+ let findings = PluginEngine.evaluate(exchange(), manifests: manifests)
+ XCTAssertFalse(findings.contains { $0.pluginID == "traffic.large-upload" })
+ XCTAssertTrue(findings.contains { $0.pluginID == "traffic.risky-method" })
+ }
+
+ func testFindingsPersistHideWhenDisabledAndPruneWithExchange() throws {
+ let db = try database()
+ let id = try db.insertExchange(exchange())
+ let visible = try db.pluginFindings(exchangeIDs: [id])
+ XCTAssertNotNil(visible[id]?.first { $0.pluginID == "traffic.large-upload" })
+
+ try db.setPluginEnabled(id: "traffic.large-upload", enabled: false)
+ XCTAssertNil(try db.pluginFindings(exchangeIDs: [id])[id]?.first { $0.pluginID == "traffic.large-upload" })
+ XCTAssertNotNil(try db.pluginFindings(exchangeIDs: [id], visibleOnly: false)[id]?.first { $0.pluginID == "traffic.large-upload" })
+
+ try db.pruneExchanges(olderThan: Date().addingTimeInterval(60))
+ XCTAssertTrue(try db.pluginFindings(exchangeIDs: [id], visibleOnly: false).isEmpty)
+ }
+
+ func testManifestTracksOfficialCommonGuardrailProviders() throws {
+ var manifest = PluginManifest(id: "official.presidio-pii", name: "Presidio PII", version: "1.0.0", kind: .llmMCP,
+ guardrailProvider: .presidio,
+ description: "Detects and masks PII.", privacySummary: "Uses redacted inspection metadata.")
+ manifest.source = .installed
+ manifest.publisher = .official
+ let data = try JSONEncoder().encode(manifest)
+ let decoded = try JSONDecoder().decode(PluginManifest.self, from: data)
+ XCTAssertEqual(decoded.publisher, .official)
+ XCTAssertEqual(decoded.guardrailProvider, .presidio)
+ XCTAssertEqual(PluginManifest.GuardrailProvider.prismaAIRS.title, "PANW Prisma AIRS")
+ }
+}
diff --git a/docs/404.html b/docs/404.html
index 0e233050..f4747c3e 100644
--- a/docs/404.html
+++ b/docs/404.html
@@ -38,6 +38,7 @@
AI agents
Docs
+ Plugins
Privacy
Threat model
Releases
@@ -85,7 +86,7 @@ That page isn't here Try the home page ,
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.13.8 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.14.0 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/about/index.html b/docs/about/index.html
index ade50096..27945bd1 100644
--- a/docs/about/index.html
+++ b/docs/about/index.html
@@ -58,6 +58,7 @@
AI agents
Docs
+ Plugins
Privacy
Threat model
Releases
@@ -152,7 +153,7 @@ Thanks
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.13.8 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.14.0 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/de/about/index.html b/docs/de/about/index.html
index 75760268..e57b0d00 100644
--- a/docs/de/about/index.html
+++ b/docs/de/about/index.html
@@ -58,6 +58,7 @@
KI-Agenten
Dokumentation
+ Plugins
Datenschutz
Bedrohungsmodell
Versionen
@@ -151,7 +152,7 @@ Dank
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.13.8 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.14.0 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/docs/index.html b/docs/de/docs/index.html
index cc9e2ce8..2efdc766 100644
--- a/docs/de/docs/index.html
+++ b/docs/de/docs/index.html
@@ -58,6 +58,7 @@
KI-Agenten
Dokumentation
+ Plugins
Datenschutz
Bedrohungsmodell
Versionen
@@ -75,7 +76,7 @@
Dokumentation
Flowlight benutzen
-
Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.13.8, macOS 15 oder neuer.
+
Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.14.0, macOS 15 oder neuer.
@@ -752,7 +753,7 @@ Grenzen
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.13.8 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.14.0 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/index.html b/docs/de/index.html
index 2b65dacc..78625a43 100644
--- a/docs/de/index.html
+++ b/docs/de/index.html
@@ -48,7 +48,7 @@
-
+
Zum Inhalt springen
@@ -58,6 +58,7 @@
KI-Agenten
Dokumentation
+ Plugins
Datenschutz
Bedrohungsmodell
Versionen
@@ -89,7 +90,7 @@ Sieh, was deine Apps im Netz tun.brew install --cask xinbetween/tap/flowlightKopieren
- v0.13.8 macOS 15+ Universal GPL-3.0 Keine Telemetrie
+ v0.14.0 macOS 15+ Universal GPL-3.0 Keine Telemetrie
@@ -570,7 +571,7 @@ Wisse, was deinen Mac verlässt.
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.13.8 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.14.0 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/privacy/index.html b/docs/de/privacy/index.html
index 8b517d68..1f868036 100644
--- a/docs/de/privacy/index.html
+++ b/docs/de/privacy/index.html
@@ -58,6 +58,7 @@
KI-Agenten
Dokumentation
+ Plugins
Datenschutz
Bedrohungsmodell
Versionen
@@ -187,7 +188,7 @@
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.13.8 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.14.0 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/threat-model/index.html b/docs/de/threat-model/index.html
index ce583894..e821bfa3 100644
--- a/docs/de/threat-model/index.html
+++ b/docs/de/threat-model/index.html
@@ -58,6 +58,7 @@
KI-Agenten
Dokumentation
+ Plugins
Datenschutz
Bedrohungsmodell
Versionen
@@ -227,7 +228,7 @@ Eine Schwachstelle melden
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.13.8 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.14.0 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/docs/index.html b/docs/docs/index.html
index ef2849d7..737bb9ae 100644
--- a/docs/docs/index.html
+++ b/docs/docs/index.html
@@ -58,6 +58,7 @@
AI agents
Docs
+ Plugins
Privacy
Threat model
Releases
@@ -75,7 +76,7 @@
Documentation
Using Flowlight
-
Everything from the first launch to tuning the agent rules. Flowlight 0.13.8, macOS 15 or later.
+
Everything from the first launch to tuning the agent rules. Flowlight 0.14.0, macOS 15 or later.
@@ -758,7 +759,7 @@ Limitations
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.13.8 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.14.0 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/es/about/index.html b/docs/es/about/index.html
index 63139031..847fa3bf 100644
--- a/docs/es/about/index.html
+++ b/docs/es/about/index.html
@@ -58,6 +58,7 @@
Agentes de IA
Documentación
+ Plugins
Privacidad
Modelo de amenazas
Versiones
@@ -151,7 +152,7 @@ Agradecimientos
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.13.8 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.14.0 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/es/docs/index.html b/docs/es/docs/index.html
index f48981b5..07e3a0db 100644
--- a/docs/es/docs/index.html
+++ b/docs/es/docs/index.html
@@ -58,6 +58,7 @@
Agentes de IA
Documentación
+ Plugins
Privacidad
Modelo de amenazas
Versiones
@@ -75,7 +76,7 @@
Documentación
Usar Flowlight
-
Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.13.8, macOS 15 o posterior.
+
Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.14.0, macOS 15 o posterior.
@@ -746,7 +747,7 @@ Limitaciones
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.13.8 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.14.0 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/es/index.html b/docs/es/index.html
index d393c11a..c6dde3c8 100644
--- a/docs/es/index.html
+++ b/docs/es/index.html
@@ -48,7 +48,7 @@
-
+
Ir al contenido
@@ -58,6 +58,7 @@
Agentes de IA
Documentación
+ Plugins
Privacidad
Modelo de amenazas
Versiones
@@ -89,7 +90,7 @@ Observa la actividad de red de tus apps.
brew install --cask xinbetween/tap/flowlightCopiar
- v0.13.8 macOS 15+ Universal GPL-3.0 Sin telemetría
+ v0.14.0 macOS 15+ Universal GPL-3.0 Sin telemetría
@@ -570,7 +571,7 @@ Ten claro qué sale de tu Mac.
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.13.8 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.14.0 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/es/privacy/index.html b/docs/es/privacy/index.html
index 4bac13d9..6047ccad 100644
--- a/docs/es/privacy/index.html
+++ b/docs/es/privacy/index.html
@@ -58,6 +58,7 @@
Agentes de IA
Documentación
+ Plugins
Privacidad
Modelo de amenazas
Versiones
@@ -187,7 +188,7 @@
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.13.8 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.14.0 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/es/threat-model/index.html b/docs/es/threat-model/index.html
index e7eb23b4..8f3a2d76 100644
--- a/docs/es/threat-model/index.html
+++ b/docs/es/threat-model/index.html
@@ -58,6 +58,7 @@
Agentes de IA
Documentación
+ Plugins
Privacidad
Modelo de amenazas
Versiones
@@ -222,7 +223,7 @@ Informar de una vulnerabilidad
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.13.8 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.14.0 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/fr/about/index.html b/docs/fr/about/index.html
index 7c618fed..5c67fd0a 100644
--- a/docs/fr/about/index.html
+++ b/docs/fr/about/index.html
@@ -58,6 +58,7 @@
Agents IA
Documentation
+ Plugins
Confidentialité
Modèle de menaces
Versions
@@ -151,7 +152,7 @@ Remerciements
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.13.8 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.14.0 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/fr/docs/index.html b/docs/fr/docs/index.html
index 9038ec9b..3b8ad429 100644
--- a/docs/fr/docs/index.html
+++ b/docs/fr/docs/index.html
@@ -58,6 +58,7 @@
Agents IA
Documentation
+ Plugins
Confidentialité
Modèle de menaces
Versions
@@ -75,7 +76,7 @@
Documentation
Utiliser Flowlight
-
Tout, du premier lancement au réglage des règles des agents. Flowlight 0.13.8, macOS 15 ou version ultérieure.
+
Tout, du premier lancement au réglage des règles des agents. Flowlight 0.14.0, macOS 15 ou version ultérieure.
@@ -752,7 +753,7 @@ Limites
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.13.8 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.14.0 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/fr/index.html b/docs/fr/index.html
index 5f638136..f46c68d3 100644
--- a/docs/fr/index.html
+++ b/docs/fr/index.html
@@ -48,7 +48,7 @@
-
+
Aller au contenu
@@ -58,6 +58,7 @@
Agents IA
Documentation
+ Plugins
Confidentialité
Modèle de menaces
Versions
@@ -89,7 +90,7 @@ Voyez l’activité réseau de vos apps.
brew install --cask xinbetween/tap/flowlightCopier
- v0.13.8 macOS 15+ Universel GPL-3.0 Sans télémétrie
+ v0.14.0 macOS 15+ Universel GPL-3.0 Sans télémétrie
@@ -570,7 +571,7 @@ Sachez ce qui quitte votre Mac.
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.13.8 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.14.0 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/fr/privacy/index.html b/docs/fr/privacy/index.html
index 880e71b9..d8d2144f 100644
--- a/docs/fr/privacy/index.html
+++ b/docs/fr/privacy/index.html
@@ -58,6 +58,7 @@
Agents IA
Documentation
+ Plugins
Confidentialité
Modèle de menaces
Versions
@@ -187,7 +188,7 @@
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.13.8 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.14.0 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/fr/threat-model/index.html b/docs/fr/threat-model/index.html
index 7edc025f..d5ee24c5 100644
--- a/docs/fr/threat-model/index.html
+++ b/docs/fr/threat-model/index.html
@@ -58,6 +58,7 @@
Agents IA
Documentation
+ Plugins
Confidentialité
Modèle de menaces
Versions
@@ -225,7 +226,7 @@ Signaler une vulnérabilité
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.13.8 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.14.0 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/index.html b/docs/index.html
index bfb0ce6c..e986f7ae 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -48,7 +48,7 @@
-
+
Skip to content
@@ -58,6 +58,7 @@
AI agents
Docs
+ Plugins
Privacy
Threat model
Releases
@@ -89,7 +90,7 @@ See your apps' network activity.brew install --cask xinbetween/tap/flowlightCopy
- v0.13.8 macOS 15+ Universal GPL-3.0 No telemetry
+ v0.14.0 macOS 15+ Universal GPL-3.0 No telemetry
@@ -579,7 +580,7 @@ Know what leaves your Mac.
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.13.8 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.14.0 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/it/about/index.html b/docs/it/about/index.html
index 0b8d8ebf..e762e6ea 100644
--- a/docs/it/about/index.html
+++ b/docs/it/about/index.html
@@ -58,6 +58,7 @@
Agenti IA
Documentazione
+ Plugins
Privacy
Modello di minaccia
Versioni
@@ -151,7 +152,7 @@ Ringraziamenti
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.13.8 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.14.0 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/it/docs/index.html b/docs/it/docs/index.html
index 70fa40ac..4908ce5d 100644
--- a/docs/it/docs/index.html
+++ b/docs/it/docs/index.html
@@ -58,6 +58,7 @@
Agenti IA
Documentazione
+ Plugins
Privacy
Modello di minaccia
Versioni
@@ -75,7 +76,7 @@
Documentazione
Usare Flowlight
-
Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.13.8, macOS 15 o successivo.
+
Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.14.0, macOS 15 o successivo.
@@ -750,7 +751,7 @@ Limitazioni
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.13.8 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.14.0 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/it/index.html b/docs/it/index.html
index f586401d..dd96b8c0 100644
--- a/docs/it/index.html
+++ b/docs/it/index.html
@@ -48,7 +48,7 @@
-
+
Vai al contenuto
@@ -58,6 +58,7 @@
Agenti IA
Documentazione
+ Plugins
Privacy
Modello di minaccia
Versioni
@@ -89,7 +90,7 @@ Vedi la rete delle tue app.Ca
Metti una stella su GitHub
brew install --cask xinbetween/tap/flowlightCopia
- v0.13.8 macOS 15+ Universale GPL-3.0 Nessuna telemetria
+ v0.14.0 macOS 15+ Universale GPL-3.0 Nessuna telemetria
@@ -570,7 +571,7 @@ Sappi che cosa esce dal tuo Mac.
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.13.8 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.14.0 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/it/privacy/index.html b/docs/it/privacy/index.html
index f9fda606..8e4253f2 100644
--- a/docs/it/privacy/index.html
+++ b/docs/it/privacy/index.html
@@ -58,6 +58,7 @@
Agenti IA
Documentazione
+ Plugins
Privacy
Modello di minaccia
Versioni
@@ -187,7 +188,7 @@
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.13.8 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.14.0 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/it/threat-model/index.html b/docs/it/threat-model/index.html
index 34fe6e9c..397244e5 100644
--- a/docs/it/threat-model/index.html
+++ b/docs/it/threat-model/index.html
@@ -58,6 +58,7 @@
Agenti IA
Documentazione
+ Plugins
Privacy
Modello di minaccia
Versioni
@@ -223,7 +224,7 @@ Segnalare una vulnerabilità
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.13.8 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.14.0 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/ja/about/index.html b/docs/ja/about/index.html
index 43a22d26..5e4743de 100644
--- a/docs/ja/about/index.html
+++ b/docs/ja/about/index.html
@@ -58,6 +58,7 @@
AI エージェント
ドキュメント
+ Plugins
プライバシー
脅威モデル
リリース
@@ -152,7 +153,7 @@ 謝辞
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.13.8 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.14.0 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/docs/index.html b/docs/ja/docs/index.html
index 20ee938c..e1b9711d 100644
--- a/docs/ja/docs/index.html
+++ b/docs/ja/docs/index.html
@@ -58,6 +58,7 @@
AI エージェント
ドキュメント
+ Plugins
プライバシー
脅威モデル
リリース
@@ -75,7 +76,7 @@
ドキュメント
Flowlight の使い方
-
初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.13.8、macOS 15 以降。
+
初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.14.0、macOS 15 以降。
@@ -700,7 +701,7 @@ 制限事項
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.13.8 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.14.0 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/index.html b/docs/ja/index.html
index 37741384..4f6644aa 100644
--- a/docs/ja/index.html
+++ b/docs/ja/index.html
@@ -48,7 +48,7 @@
-
+
本文へスキップ
@@ -58,6 +58,7 @@
AI エージェント
ドキュメント
+ Plugins
プライバシー
脅威モデル
リリース
@@ -89,7 +90,7 @@ アプリの通信が見える。brew install --cask xinbetween/tap/flowlightコピー
- v0.13.8 macOS 15+ ユニバーサル GPL-3.0 テレメトリなし
+ v0.14.0 macOS 15+ ユニバーサル GPL-3.0 テレメトリなし
@@ -569,7 +570,7 @@ Mac から何が出ていくのかを知る。
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.13.8 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.14.0 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/privacy/index.html b/docs/ja/privacy/index.html
index 9d86a958..539e8186 100644
--- a/docs/ja/privacy/index.html
+++ b/docs/ja/privacy/index.html
@@ -58,6 +58,7 @@
AI エージェント
ドキュメント
+ Plugins
プライバシー
脅威モデル
リリース
@@ -185,7 +186,7 @@
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.13.8 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.14.0 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/threat-model/index.html b/docs/ja/threat-model/index.html
index d6e9b8ff..f5744174 100644
--- a/docs/ja/threat-model/index.html
+++ b/docs/ja/threat-model/index.html
@@ -58,6 +58,7 @@
AI エージェント
ドキュメント
+ Plugins
プライバシー
脅威モデル
リリース
@@ -224,7 +225,7 @@ 脆弱性を報告する
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.13.8 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.14.0 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ko/about/index.html b/docs/ko/about/index.html
index 618450de..3637c4b9 100644
--- a/docs/ko/about/index.html
+++ b/docs/ko/about/index.html
@@ -58,6 +58,7 @@
AI 에이전트
문서
+ Plugins
개인정보
위협 모델
릴리스
@@ -152,7 +153,7 @@ 감사
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.13.8 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.14.0 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/docs/index.html b/docs/ko/docs/index.html
index 64e9f0d3..7d3adec0 100644
--- a/docs/ko/docs/index.html
+++ b/docs/ko/docs/index.html
@@ -58,6 +58,7 @@
AI 에이전트
문서
+ Plugins
개인정보
위협 모델
릴리스
@@ -75,7 +76,7 @@
문서
Flowlight 사용하기
-
첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.13.8, macOS 15 이상.
+
첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.14.0, macOS 15 이상.
@@ -742,7 +743,7 @@ 한계
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.13.8 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.14.0 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/index.html b/docs/ko/index.html
index c8533a55..c86e5fa2 100644
--- a/docs/ko/index.html
+++ b/docs/ko/index.html
@@ -48,7 +48,7 @@
-
+
본문으로 이동
@@ -58,6 +58,7 @@
AI 에이전트
문서
+ Plugins
개인정보
위협 모델
릴리스
@@ -89,7 +90,7 @@ 앱이 무엇을 하는지 봅니다.
brew install --cask xinbetween/tap/flowlight복사
- v0.13.8 macOS 15+ 유니버설 GPL-3.0 텔레메트리 없음
+ v0.14.0 macOS 15+ 유니버설 GPL-3.0 텔레메트리 없음
@@ -570,7 +571,7 @@ 내 Mac에서 무엇이 나가는지 아세요.
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.13.8 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.14.0 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/privacy/index.html b/docs/ko/privacy/index.html
index c938d219..7c84b4bb 100644
--- a/docs/ko/privacy/index.html
+++ b/docs/ko/privacy/index.html
@@ -58,6 +58,7 @@
AI 에이전트
문서
+ Plugins
개인정보
위협 모델
릴리스
@@ -188,7 +189,7 @@
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.13.8 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.14.0 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/threat-model/index.html b/docs/ko/threat-model/index.html
index e2116e5a..e751faaa 100644
--- a/docs/ko/threat-model/index.html
+++ b/docs/ko/threat-model/index.html
@@ -58,6 +58,7 @@
AI 에이전트
문서
+ Plugins
개인정보
위협 모델
릴리스
@@ -222,7 +223,7 @@ 취약점 신고하기
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.13.8 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.14.0 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/llms-full.txt b/docs/llms-full.txt
index 79d4b82f..27555635 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.13.8, macOS 15 or later.
+ Everything from the first launch to tuning the agent rules. Flowlight 0.14.0, macOS 15 or later.
On this page
@@ -853,7 +853,7 @@ Understand your AI agents.
brew install --cask xinbetween/tap/flowlightCopy
- v0.13.8macOS 15+UniversalGPL-3.0No telemetry
+ v0.14.0macOS 15+UniversalGPL-3.0No telemetry
connectionslive
@@ -1302,6 +1302,123 @@ Questions
---
+# Flowlight plugins
+URL: https://flowlight.xinbetween.com/plugins/
+
+Plugins
+
+ Write inspection plugins
+
+ Plugins turn inspected exchanges into local advisory findings. Official Flowlight plugins cover common traffic and LLM/MCP guardrail patterns; third-party plugins use the same manifest shape.
+
+ On this page
+
+ Model
+ Trust boundaryManifestFindings
+
+ Guardrails
+ ProvidersTemplate packageReview checklist
+
+ 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.
+
+ 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.
+
+{
+ "id": "official.presidio-pii",
+ "name": "Presidio PII",
+ "version": "1.0.0",
+ "kind": "llmMCP",
+ "enabled": true,
+ "source": "installed",
+ "publisher": "official",
+ "guardrailProvider": "presidio",
+ "description": "Flags requests that should be reviewed by a PII masking policy.",
+ "ruleVersion": 1,
+ "privacySummary": "Uses header names, sizes, tool names and bounded redacted evidence only.",
+ "capabilities": ["annotate", "suggestGuardrail"]
+}
+
+ FieldValuesMeaning
+
+ kindtraffic, llmMCPWhether the plugin describes ordinary inspected HTTP or model/tool/MCP activity.
+
+ publisherofficial, thirdPartyOfficial plugins are released by Flowlight; third-party plugins are authored outside the project.
+
+ sourcebuiltIn, installedBuilt-in plugins ship in the app. Installed plugins come from a package.
+
+ capabilitiesannotate, suggestGuardrailCapabilities are advisory; enforcement still uses Flowlight rules and guardrails.
+
+ Findings
+
+ A finding is a bounded record attached to one inspected exchange. Evidence should name facts, not leak content: method, host, redacted path shape, byte counts, header names, model provider, tool names, MCP server names, approval mode and policy labels. Do not store header values, raw bodies, credentials, full tool inputs, prompts, completions or unredacted query strings.
+
+{
+ "severity": "medium",
+ "title": "High-risk tool available",
+ "summary": "Claude Code declared Bash. Add a guardrail if this tool should be refused.",
+ "evidence": [
+ { "label": "Tool", "value": "Bash" },
+ { "label": "Provider", "value": "Flowlight" }
+ ],
+ "suggestedGuardrail": {
+ "agent": "claude",
+ "server": "",
+ "tool": "Bash",
+ "origin": "observed"
+ }
+}
+
+ Supported guardrail providers
+
+ Official and third-party plugins can declare the guardrail provider they model. Declaring a provider does not send traffic to that provider or enable enforcement on its own; it is a label for the policy semantics and review path.
+
+ ProviderUse
+
+ PresidioPII detection and masking.
+
+ BedrockAWS Bedrock guardrails.
+
+ LakeraContent moderation.
+
+ AporiaCustom guardrails.
+
+ NomaNoma Security policies.
+
+ PANW Prisma AIRSPrisma AIRS guardrails.
+
+ CustomYour own guardrail implementation.
+
+ 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.
+
+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.
+
+ Review checklist
+
+ The plugin is advisory unless it creates a normal Flowlight rule or guardrail for the user to review.
+
+ Evidence is bounded and redacted; no raw bodies, prompts, completions, header values or tool inputs.
+
+ Provider-side MCP connector findings say local network rules cannot reach provider-run calls.
+
+ Failures are fail-open: capture remains complete and no finding is better than blocking capture.
+
+ The privacy summary is specific enough that a user can tell what data the plugin needs.
+
+---
+
# Flowlight privacy
URL: https://flowlight.xinbetween.com/privacy/
@@ -1419,8 +1536,25 @@ Releases
Downloads, checksums and full notes for each version are on GitHub Releases.
+ 0.14.0
+October 9, 2026Latest
+
+ 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
+ connectors and high-risk LLM tools.
+
+ Common guardrail suggestions. LLM/MCP plugin findings can suggest normal Flowlight guardrails
+ for risky tools, but enforcement still lives in the existing Guardrails system and only starts when you save
+ or enable one. Supported provider labels now include Presidio, Bedrock, Lakera, Aporia, Noma, PANW Prisma AIRS
+ and Custom.
+
+ A plugin authoring path. Settings has a Plugins tab, Inspect shows plugin findings on recorded
+ exchanges, the site documents plugin manifests and evidence limits, and templates/inspection-plugin
+ gives authors a local starter package and validator.
+
0.13.8
-October 7, 2026Latest
+October 7, 2026
Faithful static mocks from inspected traffic. Mock This Endpoint now prepopulates
retained, redacted request and response headers as editable key/value rows. Response text and JSON are
diff --git a/docs/llms.txt b/docs/llms.txt
index ec3705bf..b4bf8a63 100644
--- a/docs/llms.txt
+++ b/docs/llms.txt
@@ -1,12 +1,13 @@
# 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.13.8.
+> 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.
- [Download Flowlight.dmg](https://github.com/xinbetween/flowlight/releases/latest/download/Flowlight.dmg)
- [Source code](https://github.com/xinbetween/flowlight)
- [About Flowlight](https://flowlight.xinbetween.com/about/): Why Flowlight exists, the principles it's built on, and who makes it.
- [Flowlight documentation](https://flowlight.xinbetween.com/docs/): Install Flowlight, name your traffic, read the views, tune the AI agent rules, and uninstall cleanly.
- [Flowlight for macOS](https://flowlight.xinbetween.com/): Flowlight is open-source application-aware network intelligence for macOS, with deeper visibility into AI agents, tools and MCP servers.
+- [Flowlight plugins](https://flowlight.xinbetween.com/plugins/): How Flowlight plugins add local advisory findings for inspected traffic and LLM/MCP guardrails.
- [Flowlight privacy](https://flowlight.xinbetween.com/privacy/): What Flowlight records, where it is stored, and which external services the app contacts.
- [Flowlight releases](https://flowlight.xinbetween.com/releases/): Release notes for every version of Flowlight.
- [Flowlight threat model](https://flowlight.xinbetween.com/threat-model/): What Flowlight defends against, what it cannot, and why — monitoring rather than prevention.
diff --git a/docs/plugins/index.html b/docs/plugins/index.html
new file mode 100644
index 00000000..a0a9f172
--- /dev/null
+++ b/docs/plugins/index.html
@@ -0,0 +1,253 @@
+
+
+
+
+
+Flowlight plugins
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Skip to content
+
+
+
+
Plugins
+
Write inspection plugins
+
Plugins turn inspected exchanges into local advisory findings. Official Flowlight plugins cover common traffic and LLM/MCP guardrail patterns; third-party plugins use the same manifest shape.
+
+
+
+
+ On this page
+
+
+
+
+
+ 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.
+
+ 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.
+{
+ "id": "official.presidio-pii",
+ "name": "Presidio PII",
+ "version": "1.0.0",
+ "kind": "llmMCP",
+ "enabled": true,
+ "source": "installed",
+ "publisher": "official",
+ "guardrailProvider": "presidio",
+ "description": "Flags requests that should be reviewed by a PII masking policy.",
+ "ruleVersion": 1,
+ "privacySummary": "Uses header names, sizes, tool names and bounded redacted evidence only.",
+ "capabilities": ["annotate", "suggestGuardrail"]
+}
+
+ Field Values Meaning
+ kindtraffic, llmMCPWhether the plugin describes ordinary inspected HTTP or model/tool/MCP activity.
+ publisherofficial, thirdPartyOfficial plugins are released by Flowlight; third-party plugins are authored outside the project.
+ sourcebuiltIn, installedBuilt-in plugins ship in the app. Installed plugins come from a package.
+ capabilitiesannotate, suggestGuardrailCapabilities are advisory; enforcement still uses Flowlight rules and guardrails.
+
+
+ Findings
+ A finding is a bounded record attached to one inspected exchange. Evidence should name facts, not leak content: method, host, redacted path shape, byte counts, header names, model provider, tool names, MCP server names, approval mode and policy labels. Do not store header values, raw bodies, credentials, full tool inputs, prompts, completions or unredacted query strings.
+{
+ "severity": "medium",
+ "title": "High-risk tool available",
+ "summary": "Claude Code declared Bash. Add a guardrail if this tool should be refused.",
+ "evidence": [
+ { "label": "Tool", "value": "Bash" },
+ { "label": "Provider", "value": "Flowlight" }
+ ],
+ "suggestedGuardrail": {
+ "agent": "claude",
+ "server": "",
+ "tool": "Bash",
+ "origin": "observed"
+ }
+}
+
+ Supported guardrail providers
+ Official and third-party plugins can declare the guardrail provider they model. Declaring a provider does not send traffic to that provider or enable enforcement on its own; it is a label for the policy semantics and review path.
+
+ Provider Use
+ Presidio PII detection and masking.
+ Bedrock AWS Bedrock guardrails.
+ Lakera Content moderation.
+ Aporia Custom guardrails.
+ Noma Noma Security policies.
+ PANW Prisma AIRS Prisma AIRS guardrails.
+ Custom Your own guardrail implementation.
+
+
+ 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.
+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.
+
+ Review checklist
+
+ The plugin is advisory unless it creates a normal Flowlight rule or guardrail for the user to review.
+ Evidence is bounded and redacted; no raw bodies, prompts, completions, header values or tool inputs.
+ Provider-side MCP connector findings say local network rules cannot reach provider-run calls.
+ Failures are fail-open: capture remains complete and no finding is better than blocking capture.
+ The privacy summary is specific enough that a user can tell what data the plugin needs.
+
+
+
+
+
+
+
+
+
diff --git a/docs/privacy/index.html b/docs/privacy/index.html
index 03069c2b..0add57e3 100644
--- a/docs/privacy/index.html
+++ b/docs/privacy/index.html
@@ -58,6 +58,7 @@
AI agents
Docs
+ Plugins
Privacy
Threat model
Releases
@@ -194,7 +195,7 @@
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.13.8 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.14.0 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/pt-PT/about/index.html b/docs/pt-PT/about/index.html
index b3f5f3b5..d8883b61 100644
--- a/docs/pt-PT/about/index.html
+++ b/docs/pt-PT/about/index.html
@@ -58,6 +58,7 @@
Agentes de IA
Documentação
+ Plugins
Privacidade
Modelo de ameaças
Versões
@@ -151,7 +152,7 @@ Agradecimentos
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.13.8 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.14.0 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/pt-PT/docs/index.html b/docs/pt-PT/docs/index.html
index 35bc47bb..069e5af9 100644
--- a/docs/pt-PT/docs/index.html
+++ b/docs/pt-PT/docs/index.html
@@ -58,6 +58,7 @@
Agentes de IA
Documentação
+ Plugins
Privacidade
Modelo de ameaças
Versões
@@ -75,7 +76,7 @@
Documentação
Usar o Flowlight
-
Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.13.8, macOS 15 ou posterior.
+
Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.14.0, macOS 15 ou posterior.
@@ -747,7 +748,7 @@ Limitações
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.13.8 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.14.0 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/pt-PT/index.html b/docs/pt-PT/index.html
index b7214728..bc0afe68 100644
--- a/docs/pt-PT/index.html
+++ b/docs/pt-PT/index.html
@@ -48,7 +48,7 @@
-
+
Ir para o conteúdo
@@ -58,6 +58,7 @@
Agentes de IA
Documentação
+ Plugins
Privacidade
Modelo de ameaças
Versões
@@ -89,7 +90,7 @@ Veja a atividade de rede das suas apps.
brew install --cask xinbetween/tap/flowlightCopiar
- v0.13.8 macOS 15+ Universal GPL-3.0 Sem telemetria
+ v0.14.0 macOS 15+ Universal GPL-3.0 Sem telemetria
@@ -570,7 +571,7 @@ Saiba o que sai do seu Mac.
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.13.8 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.14.0 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/pt-PT/privacy/index.html b/docs/pt-PT/privacy/index.html
index 7a0cd557..67068e7b 100644
--- a/docs/pt-PT/privacy/index.html
+++ b/docs/pt-PT/privacy/index.html
@@ -58,6 +58,7 @@
Agentes de IA
Documentação
+ Plugins
Privacidade
Modelo de ameaças
Versões
@@ -187,7 +188,7 @@
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.13.8 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.14.0 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/pt-PT/threat-model/index.html b/docs/pt-PT/threat-model/index.html
index 85992fc1..81f82472 100644
--- a/docs/pt-PT/threat-model/index.html
+++ b/docs/pt-PT/threat-model/index.html
@@ -58,6 +58,7 @@
Agentes de IA
Documentação
+ Plugins
Privacidade
Modelo de ameaças
Versões
@@ -224,7 +225,7 @@ Comunicar uma vulnerabilidade
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.13.8 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.14.0 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/releases/index.html b/docs/releases/index.html
index cdb5c8a7..413fca4b 100644
--- a/docs/releases/index.html
+++ b/docs/releases/index.html
@@ -28,7 +28,7 @@
-
+
Skip to content
@@ -38,6 +38,7 @@
AI agents
Docs
+ Plugins
Privacy
Threat model
Releases
@@ -60,7 +61,24 @@ What's new
- 0.13.8 October 7, 2026 Latest
+ 0.14.0 October 9, 2026 Latest
+
+ 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
+ connectors and high-risk LLM tools.
+ Common guardrail suggestions. LLM/MCP plugin findings can suggest normal Flowlight guardrails
+ for risky tools, but enforcement still lives in the existing Guardrails system and only starts when you save
+ or enable one. Supported provider labels now include Presidio, Bedrock, Lakera, Aporia, Noma, PANW Prisma AIRS
+ and Custom.
+ A plugin authoring path. Settings has a Plugins tab, Inspect shows plugin findings on recorded
+ exchanges, the site documents plugin manifests and evidence limits, and templates/inspection-plugin
+ gives authors a local starter package and validator.
+
+
+
+
+
Faithful static mocks from inspected traffic. Mock This Endpoint now prepopulates
retained, redacted request and response headers as editable key/value rows. Response text and JSON are
@@ -1123,7 +1141,7 @@ What's new
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.13.8 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.14.0 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/sitemap.xml b/docs/sitemap.xml
index 35764afc..3e3ec12a 100644
--- a/docs/sitemap.xml
+++ b/docs/sitemap.xml
@@ -3,8 +3,9 @@
https://flowlight.xinbetween.com/about/ 2026-09-25
https://flowlight.xinbetween.com/docs/ 2026-10-07
https://flowlight.xinbetween.com/ 2026-10-07
+ https://flowlight.xinbetween.com/plugins/ 2026-10-09
https://flowlight.xinbetween.com/privacy/ 2026-10-07
- https://flowlight.xinbetween.com/releases/ 2026-10-07
+ https://flowlight.xinbetween.com/releases/ 2026-10-09
https://flowlight.xinbetween.com/threat-model/ 2026-09-27
https://flowlight.xinbetween.com/de/about/ 2026-09-26
https://flowlight.xinbetween.com/de/docs/ 2026-09-28
diff --git a/docs/threat-model/index.html b/docs/threat-model/index.html
index cd76a943..acc82e02 100644
--- a/docs/threat-model/index.html
+++ b/docs/threat-model/index.html
@@ -58,6 +58,7 @@
AI agents
Docs
+ Plugins
Privacy
Threat model
Releases
@@ -223,7 +224,7 @@ Reporting a vulnerability
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.13.8 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.14.0 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/zh-Hans/about/index.html b/docs/zh-Hans/about/index.html
index 13caf191..b4b1fb02 100644
--- a/docs/zh-Hans/about/index.html
+++ b/docs/zh-Hans/about/index.html
@@ -58,6 +58,7 @@
AI 代理
文档
+ Plugins
隐私
威胁模型
版本
@@ -151,7 +152,7 @@ 致谢
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.13.8 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.14.0 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/docs/index.html b/docs/zh-Hans/docs/index.html
index 376f29e6..06629415 100644
--- a/docs/zh-Hans/docs/index.html
+++ b/docs/zh-Hans/docs/index.html
@@ -58,6 +58,7 @@
AI 代理
文档
+ Plugins
隐私
威胁模型
版本
@@ -75,7 +76,7 @@
文档
使用 Flowlight
-
从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.13.8,macOS 15 或更高版本。
+
从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.14.0,macOS 15 或更高版本。
@@ -703,7 +704,7 @@ 限制
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.13.8 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.14.0 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/index.html b/docs/zh-Hans/index.html
index 28bf9028..e2b771df 100644
--- a/docs/zh-Hans/index.html
+++ b/docs/zh-Hans/index.html
@@ -48,7 +48,7 @@
-
+
跳到正文
@@ -58,6 +58,7 @@
AI 代理
文档
+ Plugins
隐私
威胁模型
版本
@@ -89,7 +90,7 @@ 看清应用的网络活动。
brew install --cask xinbetween/tap/flowlight拷贝
- v0.13.8 macOS 15+ 通用架构 GPL-3.0 无遥测
+ v0.14.0 macOS 15+ 通用架构 GPL-3.0 无遥测
@@ -567,7 +568,7 @@ 知道什么离开了你的 Mac。
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.13.8 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.14.0 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/privacy/index.html b/docs/zh-Hans/privacy/index.html
index 9e0734ce..c3e380fb 100644
--- a/docs/zh-Hans/privacy/index.html
+++ b/docs/zh-Hans/privacy/index.html
@@ -58,6 +58,7 @@
AI 代理
文档
+ Plugins
隐私
威胁模型
版本
@@ -185,7 +186,7 @@
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.13.8 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.14.0 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/threat-model/index.html b/docs/zh-Hans/threat-model/index.html
index 6437e86b..57a64403 100644
--- a/docs/zh-Hans/threat-model/index.html
+++ b/docs/zh-Hans/threat-model/index.html
@@ -58,6 +58,7 @@
AI 代理
文档
+ Plugins
隐私
威胁模型
版本
@@ -211,7 +212,7 @@ 报告漏洞
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.13.8 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.14.0 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hant/about/index.html b/docs/zh-Hant/about/index.html
index 37694afc..d02fd5a6 100644
--- a/docs/zh-Hant/about/index.html
+++ b/docs/zh-Hant/about/index.html
@@ -58,6 +58,7 @@
AI 代理
說明文件
+ Plugins
隱私
威脅模型
版本
@@ -151,7 +152,7 @@ 致謝
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.13.8 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.14.0 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/docs/index.html b/docs/zh-Hant/docs/index.html
index c4dbabdb..a9300ebc 100644
--- a/docs/zh-Hant/docs/index.html
+++ b/docs/zh-Hant/docs/index.html
@@ -58,6 +58,7 @@
AI 代理
說明文件
+ Plugins
隱私
威脅模型
版本
@@ -75,7 +76,7 @@
說明文件
使用 Flowlight
-
從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.13.8,macOS 15 或以上版本。
+
從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.14.0,macOS 15 或以上版本。
@@ -709,7 +710,7 @@ 限制
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.13.8 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.14.0 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/index.html b/docs/zh-Hant/index.html
index e39e05fd..3636e889 100644
--- a/docs/zh-Hant/index.html
+++ b/docs/zh-Hant/index.html
@@ -48,7 +48,7 @@
-
+
跳至內容
@@ -58,6 +58,7 @@
AI 代理
說明文件
+ Plugins
隱私
威脅模型
版本
@@ -89,7 +90,7 @@ 看見每個 App 的網路活動。brew install --cask xinbetween/tap/flowlight拷貝
- v0.13.8 macOS 15+ 通用架構 GPL-3.0 無遙測
+ v0.14.0 macOS 15+ 通用架構 GPL-3.0 無遙測
@@ -568,7 +569,7 @@ 知道有什麼離開了你的 Mac。
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.13.8 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.14.0 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/privacy/index.html b/docs/zh-Hant/privacy/index.html
index 075a1a1c..71bb7833 100644
--- a/docs/zh-Hant/privacy/index.html
+++ b/docs/zh-Hant/privacy/index.html
@@ -58,6 +58,7 @@
AI 代理
說明文件
+ Plugins
隱私
威脅模型
版本
@@ -185,7 +186,7 @@
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.13.8 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.14.0 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/threat-model/index.html b/docs/zh-Hant/threat-model/index.html
index 4405d0fe..bafba42f 100644
--- a/docs/zh-Hant/threat-model/index.html
+++ b/docs/zh-Hant/threat-model/index.html
@@ -58,6 +58,7 @@
AI 代理
說明文件
+ Plugins
隱私
威脅模型
版本
@@ -212,7 +213,7 @@ 回報漏洞
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.13.8 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.14.0 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/project.yml b/project.yml
index 69778fa9..ad0d9e0d 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.13.8"
- CURRENT_PROJECT_VERSION: "29"
+ MARKETING_VERSION: "0.14.0"
+ CURRENT_PROJECT_VERSION: "30"
targets:
Flowlight:
type: application
diff --git a/site/i18n/de.json b/site/i18n/de.json
index ccf3d2be..18dcb471 100644
--- a/site/i18n/de.json
+++ b/site/i18n/de.json
@@ -7,6 +7,7 @@
"nav.language": "Sprache",
"nav.agents": "KI-Agenten",
"nav.docs": "Dokumentation",
+ "nav.plugins": "Plugins",
"nav.privacy": "Datenschutz",
"nav.threat-model": "Bedrohungsmodell",
"nav.releases": "Versionen",
diff --git a/site/i18n/en.json b/site/i18n/en.json
index cfd53e96..6e99e41d 100644
--- a/site/i18n/en.json
+++ b/site/i18n/en.json
@@ -7,6 +7,7 @@
"nav.language": "Language",
"nav.agents": "AI agents",
"nav.docs": "Docs",
+ "nav.plugins": "Plugins",
"nav.privacy": "Privacy",
"nav.threat-model": "Threat model",
"nav.releases": "Releases",
diff --git a/site/i18n/es.json b/site/i18n/es.json
index faece1ef..8e2f42cf 100644
--- a/site/i18n/es.json
+++ b/site/i18n/es.json
@@ -7,6 +7,7 @@
"nav.language": "Idioma",
"nav.agents": "Agentes de IA",
"nav.docs": "Documentación",
+ "nav.plugins": "Plugins",
"nav.privacy": "Privacidad",
"nav.threat-model": "Modelo de amenazas",
"nav.releases": "Versiones",
diff --git a/site/i18n/fr.json b/site/i18n/fr.json
index 27b172b8..3322f500 100644
--- a/site/i18n/fr.json
+++ b/site/i18n/fr.json
@@ -7,6 +7,7 @@
"nav.language": "Langue",
"nav.agents": "Agents IA",
"nav.docs": "Documentation",
+ "nav.plugins": "Plugins",
"nav.privacy": "Confidentialité",
"nav.threat-model": "Modèle de menaces",
"nav.releases": "Versions",
diff --git a/site/i18n/it.json b/site/i18n/it.json
index 60f210bd..22a2ad59 100644
--- a/site/i18n/it.json
+++ b/site/i18n/it.json
@@ -7,6 +7,7 @@
"nav.language": "Lingua",
"nav.agents": "Agenti IA",
"nav.docs": "Documentazione",
+ "nav.plugins": "Plugins",
"nav.privacy": "Privacy",
"nav.threat-model": "Modello di minaccia",
"nav.releases": "Versioni",
diff --git a/site/i18n/ja.json b/site/i18n/ja.json
index 2f202ef4..179c8fae 100644
--- a/site/i18n/ja.json
+++ b/site/i18n/ja.json
@@ -7,6 +7,7 @@
"nav.language": "言語",
"nav.agents": "AI エージェント",
"nav.docs": "ドキュメント",
+ "nav.plugins": "Plugins",
"nav.privacy": "プライバシー",
"nav.threat-model": "脅威モデル",
"nav.releases": "リリース",
diff --git a/site/i18n/ko.json b/site/i18n/ko.json
index 442b263f..767a5a3a 100644
--- a/site/i18n/ko.json
+++ b/site/i18n/ko.json
@@ -7,6 +7,7 @@
"nav.language": "언어",
"nav.agents": "AI 에이전트",
"nav.docs": "문서",
+ "nav.plugins": "Plugins",
"nav.privacy": "개인정보",
"nav.threat-model": "위협 모델",
"nav.releases": "릴리스",
diff --git a/site/i18n/pt-PT.json b/site/i18n/pt-PT.json
index e8867958..4a7563c5 100644
--- a/site/i18n/pt-PT.json
+++ b/site/i18n/pt-PT.json
@@ -7,6 +7,7 @@
"nav.language": "Idioma",
"nav.agents": "Agentes de IA",
"nav.docs": "Documentação",
+ "nav.plugins": "Plugins",
"nav.privacy": "Privacidade",
"nav.threat-model": "Modelo de ameaças",
"nav.releases": "Versões",
diff --git a/site/i18n/zh-Hans.json b/site/i18n/zh-Hans.json
index 4c3bc58d..d49ad090 100644
--- a/site/i18n/zh-Hans.json
+++ b/site/i18n/zh-Hans.json
@@ -7,6 +7,7 @@
"nav.language": "语言",
"nav.agents": "AI 代理",
"nav.docs": "文档",
+ "nav.plugins": "Plugins",
"nav.privacy": "隐私",
"nav.threat-model": "威胁模型",
"nav.releases": "版本",
diff --git a/site/i18n/zh-Hant.json b/site/i18n/zh-Hant.json
index 9fe10723..566f3ef6 100644
--- a/site/i18n/zh-Hant.json
+++ b/site/i18n/zh-Hant.json
@@ -7,6 +7,7 @@
"nav.language": "語言",
"nav.agents": "AI 代理",
"nav.docs": "說明文件",
+ "nav.plugins": "Plugins",
"nav.privacy": "隱私",
"nav.threat-model": "威脅模型",
"nav.releases": "版本",
diff --git a/site/pages/plugins.html b/site/pages/plugins.html
new file mode 100644
index 00000000..d67e6a68
--- /dev/null
+++ b/site/pages/plugins.html
@@ -0,0 +1,105 @@
+
+
+
+
Plugins
+
Write inspection plugins
+
Plugins turn inspected exchanges into local advisory findings. Official Flowlight plugins cover common traffic and LLM/MCP guardrail patterns; third-party plugins use the same manifest shape.
+
+
+
+
+ On this page
+
+
+
+
+
+ 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.
+
+ 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.
+{
+ "id": "official.presidio-pii",
+ "name": "Presidio PII",
+ "version": "1.0.0",
+ "kind": "llmMCP",
+ "enabled": true,
+ "source": "installed",
+ "publisher": "official",
+ "guardrailProvider": "presidio",
+ "description": "Flags requests that should be reviewed by a PII masking policy.",
+ "ruleVersion": 1,
+ "privacySummary": "Uses header names, sizes, tool names and bounded redacted evidence only.",
+ "capabilities": ["annotate", "suggestGuardrail"]
+}
+
+ Field Values Meaning
+ kindtraffic, llmMCPWhether the plugin describes ordinary inspected HTTP or model/tool/MCP activity.
+ publisherofficial, thirdPartyOfficial plugins are released by Flowlight; third-party plugins are authored outside the project.
+ sourcebuiltIn, installedBuilt-in plugins ship in the app. Installed plugins come from a package.
+ capabilitiesannotate, suggestGuardrailCapabilities are advisory; enforcement still uses Flowlight rules and guardrails.
+
+
+ Findings
+ A finding is a bounded record attached to one inspected exchange. Evidence should name facts, not leak content: method, host, redacted path shape, byte counts, header names, model provider, tool names, MCP server names, approval mode and policy labels. Do not store header values, raw bodies, credentials, full tool inputs, prompts, completions or unredacted query strings.
+{
+ "severity": "medium",
+ "title": "High-risk tool available",
+ "summary": "Claude Code declared Bash. Add a guardrail if this tool should be refused.",
+ "evidence": [
+ { "label": "Tool", "value": "Bash" },
+ { "label": "Provider", "value": "Flowlight" }
+ ],
+ "suggestedGuardrail": {
+ "agent": "claude",
+ "server": "",
+ "tool": "Bash",
+ "origin": "observed"
+ }
+}
+
+ Supported guardrail providers
+ Official and third-party plugins can declare the guardrail provider they model. Declaring a provider does not send traffic to that provider or enable enforcement on its own; it is a label for the policy semantics and review path.
+
+ Provider Use
+ Presidio PII detection and masking.
+ Bedrock AWS Bedrock guardrails.
+ Lakera Content moderation.
+ Aporia Custom guardrails.
+ Noma Noma Security policies.
+ PANW Prisma AIRS Prisma AIRS guardrails.
+ Custom Your own guardrail implementation.
+
+
+ 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.
+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.
+
+ Review checklist
+
+ The plugin is advisory unless it creates a normal Flowlight rule or guardrail for the user to review.
+ Evidence is bounded and redacted; no raw bodies, prompts, completions, header values or tool inputs.
+ Provider-side MCP connector findings say local network rules cannot reach provider-run calls.
+ Failures are fail-open: capture remains complete and no finding is better than blocking capture.
+ The privacy summary is specific enough that a user can tell what data the plugin needs.
+
+
+
+
diff --git a/site/pages/releases.html b/site/pages/releases.html
index 87298bff..a1b4e293 100644
--- a/site/pages/releases.html
+++ b/site/pages/releases.html
@@ -13,7 +13,24 @@ What's new
- 0.13.8 October 7, 2026 Latest
+ 0.14.0 October 9, 2026 Latest
+
+ 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
+ connectors and high-risk LLM tools.
+ Common guardrail suggestions. LLM/MCP plugin findings can suggest normal Flowlight guardrails
+ for risky tools, but enforcement still lives in the existing Guardrails system and only starts when you save
+ or enable one. Supported provider labels now include Presidio, Bedrock, Lakera, Aporia, Noma, PANW Prisma AIRS
+ and Custom.
+ A plugin authoring path. Settings has a Plugins tab, Inspect shows plugin findings on recorded
+ exchanges, the site documents plugin manifests and evidence limits, and templates/inspection-plugin
+ gives authors a local starter package and validator.
+
+
+
+
+
Faithful static mocks from inspected traffic. Mock This Endpoint now prepopulates
retained, redacted request and response headers as editable key/value rows. Response text and JSON are
diff --git a/site/partials/header.html b/site/partials/header.html
index 70ece23e..5f7f87b4 100644
--- a/site/partials/header.html
+++ b/site/partials/header.html
@@ -5,6 +5,7 @@
{{t:nav.agents}}
{{t:nav.docs}}
+ {{t:nav.plugins}}
{{t:nav.privacy}}
{{t:nav.threat-model}}
{{t:nav.releases}}
diff --git a/templates/inspection-plugin/README.md b/templates/inspection-plugin/README.md
new file mode 100644
index 00000000..ea7d109a
--- /dev/null
+++ b/templates/inspection-plugin/README.md
@@ -0,0 +1,32 @@
+# 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.
+
+## Trust boundary
+
+Plugins run after an inspected HTTP exchange has been recorded locally. They can add findings and suggest normal Flowlight guardrails, but they do not block, rewrite, mock, transform, upload, or export traffic by themselves.
+
+Evidence must be bounded and redacted. Do not include raw request or response bodies, prompts, completions, header values, credentials, full tool inputs, or unredacted query strings.
+
+## Files
+
+- `manifest.json`: plugin identity, publisher category, guardrail provider, privacy summary and capabilities.
+- `findings.example.json`: sample findings the plugin may produce.
+- `validate.py`: local schema and privacy sanity checks for the template package.
+
+## Supported guardrail providers
+
+- `presidio`: PII detection and masking.
+- `bedrock`: AWS Bedrock guardrails.
+- `lakera`: content moderation.
+- `aporia`: custom guardrails.
+- `noma`: Noma Security policies.
+- `prismaAIRS`: PANW Prisma AIRS guardrails.
+- `custom`: your own guardrail implementation.
+- `flowlight`: Flowlight's official built-in rules.
+
+## Validate
+
+```sh
+python3 validate.py
+```
diff --git a/templates/inspection-plugin/findings.example.json b/templates/inspection-plugin/findings.example.json
new file mode 100644
index 00000000..4301147c
--- /dev/null
+++ b/templates/inspection-plugin/findings.example.json
@@ -0,0 +1,27 @@
+[
+ {
+ "severity": "medium",
+ "title": "High-risk tool available",
+ "summary": "The agent declared a shell-like tool. Add a guardrail if this tool should be refused.",
+ "evidence": [
+ { "label": "Tool", "value": "Bash" },
+ { "label": "Provider", "value": "Flowlight" }
+ ],
+ "suggestedGuardrail": {
+ "agent": "claude",
+ "server": "",
+ "tool": "Bash",
+ "origin": "observed"
+ }
+ },
+ {
+ "severity": "low",
+ "title": "Provider-run MCP connector",
+ "summary": "The model provider offered an MCP connector. Local network rules cannot block provider-side calls unless the same host is also contacted locally.",
+ "evidence": [
+ { "label": "Provider", "value": "Anthropic" },
+ { "label": "Connector", "value": "github" },
+ { "label": "Approval", "value": "always" }
+ ]
+ }
+]
diff --git a/templates/inspection-plugin/manifest.json b/templates/inspection-plugin/manifest.json
new file mode 100644
index 00000000..82dfe77d
--- /dev/null
+++ b/templates/inspection-plugin/manifest.json
@@ -0,0 +1,14 @@
+{
+ "id": "thirdparty.example-guardrails",
+ "name": "Example Guardrails",
+ "version": "0.1.0",
+ "kind": "llmMCP",
+ "enabled": true,
+ "source": "installed",
+ "publisher": "thirdParty",
+ "guardrailProvider": "custom",
+ "description": "Example advisory findings for inspected LLM and MCP activity.",
+ "ruleVersion": 1,
+ "privacySummary": "Uses provider names, tool names, MCP server names, request sizes and redacted evidence only.",
+ "capabilities": ["annotate", "suggestGuardrail"]
+}
diff --git a/templates/inspection-plugin/validate.py b/templates/inspection-plugin/validate.py
new file mode 100644
index 00000000..8000fa6e
--- /dev/null
+++ b/templates/inspection-plugin/validate.py
@@ -0,0 +1,65 @@
+#!/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()