Files
LorenEteval_Furious/Furious/Plugins

Furious plugins

Furious.Plugins is the extension host. It defines contracts, discovery, lifecycle, and capability indexes; it does not implement a proxy protocol or runtime itself. Official implementations live in Furious.Backends, while extensions that are not proxy runtimes live in Furious.Extensions.

Plugin API version 3 is capability-based. A plugin is a metadata-bearing container for any combination of independent capabilities, not a synonym for a proxy core. The currently supported capability kinds are:

  • ProtocolHandler: recognizes one protocol, parses and exports share links, creates normalized connection documents, and validates them.
  • ProtocolEditorProvider: lazily creates a PySide6 editor for one or more protocol IDs. Protocol parsing remains usable without importing Qt.
  • SubscriptionDecoder: identifies and decodes one subscription wire format into URI or mapping entries. It does not create or persist profiles.
  • KernelFactory: recognizes runtime configuration types and constructs a prepared KernelLaunch from a KernelRequest.
  • TrafficStatsProvider: exposes an optional, worker-safe cumulative traffic counter query for runtime kernels that support monitoring.
  • ActionProvider: contributes optional management UI without making the application assume that the owning plugin is a proxy core.
  • PluginCapability: may also be subclassed for future utility extension points identified by CapabilityKind.Utility.

The registry exposes both a generic query API (capabilities, capability, and pluginsWithCapability) and focused dispatch helpers. Adding a capability does not require changing an enum of plugin types or a central protocol switch.

Metadata, declaration, and lifecycle

Every plugin declares immutable PluginMetadata and a sequence of capability objects. Capability identifiers are unique within their kind. The registry also validates protocol IDs, URI schemes, editor ownership, configuration types, and kernel types before it mutates its indexes.

from Furious.Plugins import FuriousPlugin, PluginMetadata


class ExamplePlugin(FuriousPlugin):
    metadata = PluginMetadata(
        id='example.networking',
        displayName='Example Networking',
        version='1.0',
        description='Example protocol and runtime support.',
        provider='Example Organization',
    )

    def __init__(self):
        self.capabilities = (
            ExampleProtocol(),
            ExampleProtocolEditor(),
            ExampleKernelFactory(),
        )

The host validates and indexes the complete declaration, then calls plugin.initialize(context). Failed initialization rolls registration back. During application cleanup, initialized plugins receive shutdown() in reverse order.

Third-party distributions expose a FuriousPlugin class or instance through the furious.plugins Python entry-point group:

[project.entry-points."furious.plugins"]
example = "furious_example.plugin:ExamplePlugin"

Plugins execute inside the Furious process and are trusted Python code.

Protocols and editors

A protocol handler owns connection semantics, never UI:

from Furious.Plugins import (
    ProtocolDescriptor,
    ProtocolHandler,
    ProtocolParseResult,
)


class ExampleProtocol(ProtocolHandler):
    descriptor = ProtocolDescriptor(
        id='example',
        displayName='Example',
        addActionText='Add Example Server...',
        menuOrder=100,
        configurationSchema={
            'required': ('address', 'port'),
        },
        # Third-party labels are literal by default. Official translation keys
        # opt in explicitly.
        translatable=False,
    )
    schemes = ('example',)

    def supports(self, configuration):
        return isinstance(configuration, ExampleConfig)

    def parse(self, uri, **kwargs):
        configuration, display_name = ExampleConfig.fromURI(uri)
        return ProtocolParseResult(
            configuration,
            {'displayName': display_name},
        )

    def fromMapping(self, configuration, **kwargs):
        if configuration.get('type') == 'example':
            return ExampleConfig(configuration, **kwargs)

    def blank(self, **kwargs):
        return ExampleConfig.blank(**kwargs)

    def export(self, configuration, remark=''):
        return configuration.toURI(remark)

An optional editor capability binds Qt UI to the protocol ID. It imports Qt widgets inside createEditor, because plugin discovery can occur while Furious.Qt is still initializing:

from Furious.Plugins import ProtocolEditorProvider


class ExampleProtocolEditor(ProtocolEditorProvider):
    editorId = 'example.qt-editor'
    protocolIds = ('example',)

    def createEditor(self, protocolId, parent=None, **kwargs):
        from .Editor import ExampleEditor

        return ExampleEditor(parent=parent, **kwargs)

The add/edit server UI enumerates registered ProtocolDescriptor values and asks the registry for an editor. It contains no VMess/VLESS/Trojan-specific factory switch. Headless protocol plugins may intentionally omit an editor.

Subscriptions

Subscription processing has three stages:

  1. The source layer downloads bytes and supplies source metadata.
  2. A SubscriptionDecoder recognizes the representation and emits normalized SubscriptionItem values. Plain share links and Base64 share links are separate decoders, so Clash YAML or sing-box JSON can be added independently.
  3. SubscriptionImportService dispatches each URI or mapping to a registered protocol handler, creates a ServerProfile, and combines decoder-provided item metadata with source ID and update time. UI code only displays results and updates storage.

Decoder priority controls automatic format detection. A caller may also select a decoder explicitly by ID, which avoids content-guessing for a known source.

Runtime kernels

Runtime selection is based on registered configuration types. The connection manager asks the registry to create a kernel rather than branching on Xray, Hysteria, or another core:

from Furious.Plugins import KernelFactory, KernelLaunch


class ExampleKernelFactory(KernelFactory):
    factoryId = 'example.runtime'
    configurationTypes = (ExampleConfig,)
    kernelTypes = (ExampleProcess,)

    def create(self, request):
        process = ExampleProcess(
            exitCallback=request.exitCallback,
            msgCallback=request.messageCallback,
        )
        return KernelLaunch(
            kernel=process,
            configuration=request.configuration,
            options=request.options,
        )

KernelRequest carries host-selected routing, callbacks, proxy-only mode, log preference, and start options. KernelLaunch binds the constructed process to its prepared arguments. TUN preparation, routing choices, download-test configuration, version reporting, log parsing, and post-connect maintenance are also dispatched through the selected factory.

Management actions are a distinct ActionProvider; a runtime factory therefore does not import or own Qt UI merely because its plugin also offers settings.

Traffic monitoring is similarly optional. A TrafficStatsProvider associates one or more kernel types with a TrafficStatsMonitor. The monitor contains a query callable and backend-specific target, allowing the host service to run native API calls on a lightweight background thread. The service exposes both cumulative usage and calculated rates to Qt widgets, and suspends polling while its parent widget is hidden or minimized.

Profiles and persistence

ServerProfile composes two different kinds of data:

  • connection is a ConfigFactory containing only protocol/core connection configuration.
  • metadata is ProfileMetadata, containing display name, group, tags, subscription source, update time, annotations, favorite state, and transient presentation measurements.

Storage schema version 2 persists these as separate metadata and connection fields. The repository migrates the previous combined record format while loading. Subscription synchronization can now replace a connection document without losing user metadata, and configuration export cannot accidentally serialize table state. URI parsers return a ProtocolParseResult, so even fragment-derived display names do not pass through a connection model as temporary attributes.

Routing labels are literal by default. RoutingOption.translatable should be set only when displayName is an application translation key; user-provided labels must remain literal. Protocol add-action labels follow the same rule via ProtocolDescriptor.translatable. Plugin metadata display names are always treated as literal names.