Skip to content

Universal Launcher: Action Registry, Fuzzy Search, and Plugin System

The Portal Launcher is the single entry point for every user action on Portal OS — whether triggered by keyboard shortcut, touch gesture, or voice command. It unifies application launching, system controls, spatial dimension switching, web search, and PCP capability invocation under one searchable, ranked, and execution-safe interface. This page explains the three pillars of the launcher: the Action Registry that stores every discoverable action, the Fuzzy Search Engine that ranks them by relevance, and the Plugin System that allows third-party extensions without compromising security.

The launcher is structured as a layered system where concerns are cleanly separated: providers discover and register actions, the registry stores them, the search engine ranks them, and the execution pipeline safely dispatches the chosen action to its target (PCP capability, D-Bus method, process spawn, or internal handler). A voice bridge sits alongside the pipeline to translate NLU intent classifications into the same action flow.

graph TB
    subgraph "Action Discovery"
        AP[App Provider<br/>.desktop files]
        WP[Web Provider]
        SP[Spatial Provider]
        PP[PCP Provider]
        PL[Plugin Loader<br/>TOML manifests]
    end

    subgraph "Registry Layer"
        AR[Action Registry<br/>InMemoryRegistry]
        AR --> |broadcast events| EV[RegistryEvent<br/>broadcast channel]
    end

    subgraph "Search Layer"
        SE[Search Engine]
        AI[Acronym Index]
        SE --> AI
        SE --> |Jaro-Winkler| FW[Field Weights<br/>name:0.5 kw:0.3 desc:0.2]
    end

    subgraph "Execution Pipeline"
        R1[1. Resolve]
        R2[2. Confirm]
        R3[3. Execute]
        R4[4. Grace Period]
        R5[5. Audit]
        R1 --> R2 --> R3 --> R4 --> R5
    end

    AP --> AR
    WP --> AR
    SP --> AR
    PP --> AR
    PL --> AR

    AR --> |all_actions| SE
    SE --> |SearchResult| EP[Execute Entry Point]

    EP --> R1
    R3 --> |PCP / D-Bus / Process / Internal| TGT[Execution Target]
    R5 --> TM[Telemetry<br/>lock-free atomics]

    VB[Voice Bridge<br/>IntentMap + DenyList] --> EP

    style AR fill:#2d4a7a,color:#fff
    style SE fill:#2d4a7a,color:#fff
    style R3 fill:#3a6b3a,color:#fff

At the top level, the Launcher struct wires all components together and serves as the facade through which the launcherd daemon and any IPC clients interact. It holds the registry, search engine, execution pipeline, voice bridge, telemetry collector, persistent SQLite index, and provider list — all behind Arc shared references for thread-safe concurrent access.

Sources: portal/launcher/src/lib.rs#L46-L60, portal/launcher/src/facade.rs#L21-L64, portal/launcherd/src/main.rs#L19-L26


Every launchable operation in Portal OS is represented as an Action — a self-describing struct containing its identity, search metadata, execution target, confirmation policy, and usage statistics. The key fields are:

Field Type Purpose
id ActionId Globally unique identifier (category.provider.name)
name String Display name shown in search results
keywords Vec<String> Search keywords (name + aliases)
category ActionCategory Grouping enum: App, System, Web, Spatial, Media, File, Pcp, etc.
target ExecutionTarget How the action is dispatched: PCP, D-Bus, ProcessSpawn, Internal
confirmation ActionConfirmation Voice-triggered confirmation tier
base_priority f32 Static ranking weight (0.0–1.0)
execution_count u64 Number of times executed (feeds frequency boost)
last_executed_at Option<DateTime<Utc>> Last execution timestamp (feeds recency boost)

The ActionId follows a hierarchical dot-separated convention: {category}.{provider}.{name}. At least three components are required, but the format intentionally permits four or more to support hierarchical names like system.display.brightness.set or pcp.pcp.weather.query. Each component must be lowercase alphanumeric with underscores — enforced at construction time by ActionId::new().

Sources: portal/launcher/src/action_registry/types.rs#L13-L98, portal/launcher/src/action_registry/types.rs#L220-L267

Action Categories and Confirmation Policies

Section titled “Action Categories and Confirmation Policies”

The ActionCategory enum determines how actions are grouped in the UI and filtered during search. There are nine categories, covering everything from app launching to PCP-mirrored capabilities:

pub enum ActionCategory {
App, Window, System, Web, Media, Spatial, File, Settings, Communication, Pcp,
}

Confirmation is governed by two separate policies working in concert:

Policy Applies To Values When Used
ConfirmationPolicy Keyboard/gesture triggers None, Destructive, SideEffect, System, VoiceApproval General UI execution
ActionConfirmation Voice triggers only AutoExecute, ConfirmOnVoice, AlwaysConfirm Voice bridge path

For example, a volume-up action is ConfirmationPolicy::None and ActionConfirmation::AutoExecute — it fires instantly regardless of input modality. A shutdown action is ConfirmationPolicy::Destructive and ActionConfirmation::AlwaysConfirm — it requires explicit confirmation from every source and enters a 5-second grace period during which the user can cancel.

Sources: portal/launcher/src/action_registry/types.rs#L112-L137, portal/launcher/src/action_registry/types.rs#L140-L175

Registry Implementation: Thread-Safe In-Memory Storage

Section titled “Registry Implementation: Thread-Safe In-Memory Storage”

The registry is backed by InMemoryRegistry, a parking_lot::RwLock<HashMap<ActionId, Arc<Action>>> with a tokio::sync::broadcast channel for change events. The RegistryHandle wraps this and provides a clean API surface — it is Clone, Send, and Sync, making it safe to share across async tasks.

Three traits define the registry contract:

  • ActionRegistry — Read operations: get(), contains(), all_actions(), actions_by_category(), actions_by_provider(), action_count()
  • ActionRegistryMut — Write operations: register(), unregister(), register_batch(), clear()
  • ActionRegistryEvents — Event subscription via mpsc::Receiver<RegistryEvent>

Every mutation emits a RegistryEvent (ActionsChanged, ProviderRefreshed, or FullRebuild) through the broadcast channel. Subscribers receive these events asynchronously, enabling the search engine and UI to stay in sync with registry changes without polling.

Sources: portal/launcher/src/action_registry/registry/in_memory.rs#L17-L36, portal/launcher/src/action_registry/registry/mod.rs#L34-L60, portal/launcher/src/action_registry/registry/handle.rs#L17-L46

Action Providers: How Actions Enter the Registry

Section titled “Action Providers: How Actions Enter the Registry”

Providers are the pluggable source layer — each implements the ActionProvider trait and registers its actions with the registry during launcher initialization. The trait defines a clear lifecycle:

sequenceDiagram
    participant L as Launcher
    participant P as ActionProvider
    participant R as RegistryHandle

    L->>P: register_actions(&registry)
    P->>P: Scan data source (.desktop files, PCP, etc.)
    P->>R: register(Arc<Action>) per action
    R-->>P: (implicit, events broadcast)
    P-->>L: Ok(count)

    Note over L,R: Periodic or on-demand refresh
    L->>P: refresh(&registry)
    P->>R: unregister stale, register new
    P-->>L: Ok((added, removed))

    Note over L,R: Shutdown
    L->>P: unregister_actions(&registry)
    P-->>L: Ok(count)

Four built-in providers ship with the launcher, each responsible for a different category of actions:

Provider Source Example Actions
AppProvider .desktop file parsing app.app.firefox.open, app.app.firefox.close
WebProvider Hardcoded web search action web.web.search
SpatialProvider Spatial dimension state spatial.spatial.dimension.switch
PcpProvider PCP capability registry pcp.pcp.{app}.{action}

The PCP Provider deserves special attention: it mirrors the Portal Capability Protocol’s capability registry into the launcher. Each PCP capability becomes a searchable action, with confirmation policy derived from the PCP domain — System capabilities get AlwaysConfirm, Compositor capabilities get ConfirmOnVoice, and App capabilities get AutoExecute. This means any application that registers a PCP capability automatically becomes launchable through the launcher.

Sources: portal/launcher/src/providers/mod.rs#L27-L78, portal/launcher/src/providers/pcp.rs#L14-L49, portal/launcher/src/providers/app/mod.rs#L1-L30, portal/launcherd/src/main.rs#L19-L26


Fuzzy Search Engine: Ranking Actions by Relevance

Section titled “Fuzzy Search Engine: Ranking Actions by Relevance”

When the user types a query, the search engine processes it through a multi-stage pipeline that short-circuits at the earliest possible match:

flowchart TD
    Q[User Query] --> E{Empty?}
    E -->|Yes| ER[Return empty]
    E -->|No| CALC{Calc/Convert<br/>pattern?}
    CALC -->|Yes| WR[Web reroute<br/>score: 1.0]
    CALC -->|No| AC{Acronym<br/>exact match?}
    AC -->|Yes| AS[Score & return<br/>MatchType::Exact]
    AC -->|No| AP{Acronym<br/>prefix match?}
    AP -->|Yes| APS[Field score + boost<br/>truncate & return]
    AP -->|No| FS[Full fuzzy search]
    FS --> F1[Compute field scores<br/>name, keywords, description]
    F1 --> F2[Apply context boosts]
    F1 --> F3[Composite score:<br/>fuzzy×text + priority + freq + recency + ctx]
    F3 --> F4{Score ≥ threshold?}
    F4 -->|No| FILTER[Drop]
    F4 -->|Yes| KEEP[Keep]
    KEEP --> SORT[Sort by score desc]
    SORT --> TRUNC[Truncate to limit]

The engine first checks for calculator and currency conversion patterns using compiled regexes — if the query matches ^\d+[\+\-\*\/\%]\d+ or a currency conversion like 100 usd to eur, it immediately returns a web search reroute result rather than searching the action registry. This provides instant “type-to-calculate” behavior.

Sources: portal/launcher/src/search_engine/search.rs#L16-L78, portal/launcher/src/search_engine/mod.rs#L21-L28

For each candidate action, the engine computes a text similarity score across three fields — name, keywords, and description — using the Jaro-Winkler string similarity algorithm from the strsim crate. Each field is checked in priority order:

  1. Exact match (score: 1.0) — query equals field text
  2. Prefix match (score: 0.9) — field starts with query or vice versa
  3. Fuzzy match — Jaro-Winkler similarity (0.0–1.0)

The best match kind across all fields is tracked, and the final weighted score is:

score = (weight_name × name_score) + (weight_keyword × keyword_score) + (weight_description × desc_score)

Default field weights are name: 0.5, keyword: 0.3, description: 0.2 — giving the action’s display name the highest influence on ranking.

Sources: portal/launcher/src/search_engine/ranking/scoring.rs#L47-L115, portal/launcher/src/search_engine/ranking/mod.rs#L12-L17

Text similarity alone is not enough — the launcher needs to surface frequently-used actions and account for contextual signals. The composite score formula combines five weighted components:

Component Weight (Default) Formula Purpose
Text (fuzzy) 0.5 weight_fuzzy × field_weighted_score Query relevance
Base Priority 0.2 weight_priority × action.base_priority Static importance
Frequency 0.1 weight_frequency × time_decay_frequency(...) Usage history (decaying)
Recency 0.1 weight_recency × recency_boost(...) Recent usage bonus
Context 0.1 Sum of matching RankingBoost values Situational relevance

The frequency component uses exponential time decay: count × 2^(-elapsed_days / half_life) with a 30-day half-life. This means an action executed 100 times a month ago contributes roughly the same boost as one executed 50 times today — preventing stale high-count actions from dominating.

The recency boost is linear, decaying from a maximum of 0.3 to zero over a 7-day window. This provides a gentle “recently used” nudge without overwhelming the frequency signal.

All five weights must sum to approximately 1.0 (validated at configuration load time with a ±0.05 tolerance).

Sources: portal/launcher/src/search_engine/search.rs#L192-L203, portal/launcher/src/search_engine/ranking/scoring.rs#L117-L160, portal/launcher/src/config/defaults.rs#L25-L42, portal/launcher/src/config/mod.rs#L164-L186

Acronym Index: Quick Path for Common Abbreviations

Section titled “Acronym Index: Quick Path for Common Abbreviations”

The AcronymIndex builds a lookup table from the first letters of each word in action names. For example, “Display Brightness Control” generates the acronym “dbc”. When the user types “dbc”, the search engine can bypass the full fuzzy scan and return the matching action with MatchType::Exact — significantly faster than Jaro-Winkler over the entire registry.

The index limits acronyms to the first five words of each action name to avoid overly long keys. It also supports prefix matching: typing “db” will match any acronym starting with those letters.

Sources: portal/launcher/src/search_engine/acronym.rs#L9-L79, portal/launcher/src/search_engine/search.rs#L42-L78

Context Boosts: Situational Ranking Adjustments

Section titled “Context Boosts: Situational Ranking Adjustments”

The RankingBoost mechanism allows external systems (like the Context Engine) to temporarily elevate or suppress certain actions based on the user’s current situation. Each boost specifies:

  • An action_id_pattern using component-wise glob matching (e.g., system.*.volume.* matches any volume action)
  • An optional category filter
  • An optional keyword_match filter
  • A boost value (positive or negative)
  • A ttl (time-to-live) with automatic expiry

For example, if the Context Engine detects that the user is in a spatial VR session, it might submit a RankingBoost with pattern spatial.*.*, boosting all spatial actions by +0.3 for the next hour. Expired boosts are automatically filtered out during scoring.

Sources: portal/launcher/src/search_engine/ranking/boost.rs#L1-L145, portal/launcher/src/search_engine/ranking/scoring.rs#L162-L171


Plugin System: Extensible Without Compromise

Section titled “Plugin System: Extensible Without Compromise”

Third-party plugins are defined by TOML manifest files placed in configurable directories (default: ~/.config/portal/launcher/providers/ and /etc/portal/launcher/providers/). Each manifest contains three sections:

[manifest]
id = "vendor.weather" # Format: {vendor}.{name}
name = "Weather"
version = "1.0.0"
description = "Weather plugin"
author = "Vendor"
[execution]
type = "pcp" # "pcp" or "dbus" (NOT "process")
capability = "weather.query" # Required for "pcp" type
[permissions]
capabilities = ["weather.read"]
domains = ["weather"]
[[actions]]
id = "weather.query"
name = "Check Weather"
description = "Get current weather conditions"
category = "system"

Plugins cannot spawn arbitrary processes. The "process" execution type is explicitly rejected during manifest validation — only the built-in providers retain that privilege. This is a deliberate security boundary: plugins can invoke PCP capabilities or call D-Bus methods, but they cannot execute binaries directly.

Sources: portal/launcher/src/plugin_system/manifest.rs#L1-L147, portal/launcher/src/config/defaults.rs#L96-L107

Plugin IDs follow a stricter convention than action IDs: exactly two dot-separated components (vendor.name), both lowercase alphanumeric with underscores. The vendor segment establishes a namespace that prevents collisions between different plugin authors. For example, vendor.weather and acme.weather can coexist in the same directory.

Sources: portal/launcher/src/plugin_system/manifest.rs#L126-L147

The plugin lifecycle follows a discover → validate → load pattern:

flowchart LR
    A[Scan .toml files<br/>in manifest dirs] --> B[Parse TOML]
    B -->|Valid| C[Validate ID format]
    B -->|Invalid TOML| D[Skip, log error]
    C -->|Valid| E[PluginInfo loaded]
    C -->|Invalid ID| D
    E --> F[Semaphore(4)<br/>parallel loading]
    F --> G[Insert into<br/>RwLock<HashMap>]

The PluginLoader uses a tokio::sync::Semaphore(4) to bound concurrency during parallel loading — filesystem I/O happens in spawn_blocking tasks to avoid blocking the async runtime. Invalid manifests are silently skipped (logged at warn level); they never prevent other plugins from loading.

The loader supports three runtime operations:

Operation Method Behavior
Full load load_plugins() Scans all directories, loads all valid manifests
Hot-reload reload_plugin(id) Re-scans for a specific plugin ID, replaces entry
Unload unload_plugin(id) Removes from loaded set

Sources: portal/launcher/src/plugin_system/discovery.rs#L19-L44, portal/launcher/src/plugin_system/loader.rs#L16-L141

Every plugin must declare its required capabilities and domains in the [permissions] section. The check_permission() function enforces a two-check policy: both the capability and the domain must be explicitly listed. If either is missing, execution is denied with a PermissionDenied error that includes the plugin ID and the missing permission.

Check Example Result
Capability + domain both present capabilities=["network"], domains=["weather"]check("network", "weather") ✅ Ok
Capability missing check("bluetooth", "weather") PermissionDenied
Domain missing check("network", "bluetooth") PermissionDenied

Sources: portal/launcher/src/plugin_system/permissions.rs#L15-L35


When a user selects an action from search results, it enters the DefaultExecutionPipeline — a five-stage ordered process that resolves parameters, checks confirmation requirements, dispatches to the execution target, manages grace periods, and records telemetry:

flowchart TD
    S1[Stage 1: Resolve] --> |"Registry.get(action_id)"<br/>Merge default + user params<br/>Determine target<br/>Check confirmation| S2[Stage 2: Confirm]
    S2 -->|"requires_confirmation?"| Q{Needs confirm?}
    Q -->|Yes| CB[Call ConfirmationCallback]
    CB -->|Confirmed| S3
    CB -->|Denied| PC[Store PendingConfirmation<br/>Return error]
    Q -->|No| S3[Stage 3: Execute]
    S3 --> PCP{Target type}
    PCP -->|PcpCapability| PE[PCP Executor]
    PCP -->|DbusMethod| DB[D-Bus method call]
    PCP -->|ProcessSpawn| PS[Process spawn<br/>allowlist + SHA-256]
    PCP -->|Internal| IN[Internal handler]
    S3 --> S4[Stage 4: Grace Period]
    S4 --> |"AlwaysConfirm only<br/>5-second cancel window"| S5[Stage 5: Audit]
    S5 --> TM[Telemetry.record_execution]

Sources: portal/launcher/src/execution/pipeline/execute.rs#L18-L91, portal/launcher/src/execution/pipeline/stages.rs#L12-L41

The ExecutionTarget enum defines four dispatch paths, each with its own security considerations:

Target Available To Security Measures
PcpCapability Built-in providers + plugins PCP domain-based permission checking
DbusMethod Built-in providers + plugins Whitelist of bus names (e.g., org.freedesktop.login1)
ProcessSpawn Built-in providers only Binary allow-list file + optional SHA-256 sidecar verification
Internal Built-in providers No external side effects

The ProcessSpawn path enforces two layers of security: the binary must appear in /etc/portal/launcher-allowlist.conf, and if a .sha256 sidecar file exists next to the binary, the hash is verified before execution. Environment variables injected from action parameters are restricted to an allow-list of keys (url, query, content, target, direction, amount, exec, terminal, app_id). URL parameters are validated to accept only http and https schemes.

Sources: portal/launcher/src/action_registry/types.rs#L182-L218, portal/launcher/src/execution/pipeline/stages.rs#L44-L200, portal/launcher/src/execution/pipeline/security.rs#L9-L98

Actions tagged with ActionConfirmation::AlwaysConfirm enter a 5-second grace period after execution. During this window, the GracePeriodManager tracks the action by ActionId and Instant. The user (or a voice command) can call cancel_grace_period() — if the window hasn’t expired, the action is considered reversible.

A background cleanup task runs every 60 seconds to sweep expired grace periods, preventing unbounded memory growth. The Launcher::initialize() method spawns this task, and Launcher::shutdown() aborts it.

Sources: portal/launcher/src/execution/confirm.rs#L41-L117, portal/launcher/src/facade.rs#L102-L114


The launcher reads a TOML configuration file (default path: /etc/portal/launcher.toml) that controls search weights, execution policies, UI appearance, and plugin directories. Configuration is validated at load time — search weights must sum to ~1.0, min_score_threshold must be 0.0–1.0, and max_results must be positive.

Config Section Key Defaults Controls
search weight_fuzzy: 0.5, weight_priority: 0.2, weight_frequency: 0.1, weight_recency: 0.1, weight_context: 0.1 Ranking weights, fuzzy/acronym toggles
execution voice_confirmation_policy: VoiceApproval, max_concurrent: 10, destructive_grace: 5s Confirmation, concurrency, grace period
ui visible_results: 12, theme: dark, font_scale: 1.0 Desktop UI appearance
ui.glasses visible_results: 8, font_scale: 1.2, auto_dismiss: 10s AR glasses display adaptation
providers plugins_enabled: true, 2 default directories Plugin discovery paths
web search_engine_url: duckduckgo.com, browser: firefox Web search rerouting

If the config file is missing or invalid, the launcher gracefully falls back to LauncherConfig::default() — it never fails to start due to configuration issues.

Sources: portal/launcher/src/config/mod.rs#L12-L200, portal/launcher/src/config/defaults.rs#L1-L140, portal/launcherd/src/main.rs#L77-L95


The voice subsystem integrates with the launcher through the DefaultVoiceBridge, which translates NLU intent classifications (from Elara’s MiniLM classifier) into launcher actions. The bridge contains four components:

  1. IntentMap — 30 hardcoded intent → ActionId mappings across four domains (App, Window, System, Media). Three mapping types: Fixed (direct), TargetDependent (template with {target} slot), and NotMapped (reserved for V2.1).

  2. SlotResolver — Converts IntentSlots (extracted entities from the voice transcript) into action parameters. For example, the slot target: "firefox" in an app.open intent resolves to the action ID app.app.firefox.open.

  3. DenyListChecker — Rejects transcripts containing dangerous phrases (e.g., “shut down”, “factory reset”, “uninstall”) before any action resolution occurs.

  4. TtsQueue — Queues text-to-speech feedback for voice-initiated executions.

Sources: portal/launcher/src/voice_bridge/mod.rs#L1-L94, portal/launcher/src/voice_bridge/intent_map.rs#L1-L80, portal/launcher/src/config/defaults.rs#L119-L139


All launcher metrics are collected via AtomicTelemetry — a lock-free implementation using Rust atomics. This design ensures that metrics collection never blocks the search or execution hot paths. The telemetry trait captures:

  • Search queries (count, result count, latency)
  • Search selections (query, position, total results) and abandonments
  • Action executions (action ID, source, success/failure, latency)
  • Voice resolution outcomes (intent type, NLU classification, slot resolution, execution)
  • Provider registration counts
  • Index operation latencies

All data stays on-device — nothing leaves the system. Snapshots may contain slightly stale data due to the lock-free design, but this is an acceptable trade-off for zero-latency collection.

Sources: portal/launcher/src/telemetry/mod.rs#L17-L72, portal/launcher/src/execution/audit.rs#L9-L16


When the launcher opens without a query, it shows a frequency-recency blended view of the most likely actions. Unlike search (which uses text similarity), the home view ranks purely by how often and how recently each action was used:

score = weight_recency × exp(-elapsed_secs / half_life_secs)
+ weight_frequency × (execution_count / max_execution_count)

This provides a personalized “most recently used” experience that adapts to the user’s patterns without any text input.

Sources: portal/launcher/src/facade_home.rs#L13-L69


Now that you understand how the launcher discovers, ranks, and dispatches actions, these related pages provide deeper context: