Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save airtonix/a038219ae8f8793d2b6d0fcb1d48272f to your computer and use it in GitHub Desktop.

Select an option

Save airtonix/a038219ae8f8793d2b6d0fcb1d48272f to your computer and use it in GitHub Desktop.
Option 4: rich plugin platform for hrdx — implementation plan

Option 4: rich plugin platform for hrdx

Purpose

This document develops option 4 only: a full, extensible plugin platform built from declarative integration packages, supervised external plugin peers, and a capability broker.

The target is not a larger harness.json, an event-to-shell hook system, or an in-process Go plugin ABI. A plugin is a versioned participant that can declare contributions, negotiate access, receive structured context, perform asynchronous work, and contribute useful functionality without violating hrdx's PTY-centric model.

The design deliberately combines the richest parts of the earlier directions:

  • a package manifest for discovery and inspection;
  • a long-lived external process for live integrations;
  • a bidirectional protocol for requests, responses, events, progress, and cancellation;
  • explicit capabilities and grants for security and compatibility;
  • declarative commands, contextual actions, providers, notifications, and constrained views;
  • host supervision, restart, cleanup, and persistence of user choices.

The package is the product-facing unit. The peer process is the implementation mechanism. The capability broker is the safety and compatibility boundary.


Existing hrdx constraints to preserve

The implementation must fit the current architecture rather than replace it.

  • cmd/hrdx remains the composition root and owns startup/process-mode wiring.
  • internal/ui remains the owner of live UI state and Bubble Tea update-loop mutations.
  • internal/api remains the public socket API for scripts and clients; plugin IPC must not silently gain unrestricted access to it.
  • internal/term and internal/holder continue to own real PTYs and persistent terminal sessions.
  • internal/state remains serializable core state. Plugin connections, process IDs, callbacks, and arbitrary plugin data do not belong in state.json.
  • Every terminal pane remains PTY-backed. Plugin views are a separate surface, not virtual panes. A plugin view may own its content and TUI behavior inside an hrdx floating frame or grid cell frame.
  • The binary remains portable and the dependency footprint stays small.
  • Plugin failure must not kill the TUI, existing panes, or holder sessions.
  • Events remain best-effort for rendering paths; plugin protocol work must not block the UI loop.
  • Unix and Windows need equivalent local transport and process-lifecycle behavior.

The existing harness.json mechanism remains useful for describing agent launch behavior. It is not silently redefined as the plugin system.


Product shape

What a plugin can provide

A mature plugin can combine several contributions:

  1. Commands — explicitly invokable actions with structured context and typed results.
  2. Contextual actions — commands shown for a selected workspace, tab, pane, file, or terminal context.
  3. Providers — on-demand or subscribed sources of search results, workspace candidates, completions, diagnostics, or metadata.
  4. Status contributions — bounded text, state, severity, and actions for the sidebar/status area.
  5. Notifications — user-visible messages with severity, expiry, and optional action IDs.
  6. Views — plugin-owned TUI surfaces hosted inside hrdx floating frames or grid cell frames. hrdx owns the surrounding frame, settings modal, menu pickers, and footer input.
  7. Workspace integration — read-only project discovery and optional workspace-scoped activation.
  8. Agent integration — metadata and lifecycle integration around existing PTY-backed agent panes, without taking over their terminal implementation.
  9. Private storage — namespaced plugin data, separate from core hrdx state.

The first useful plugins could include Git/project tools, issue trackers, CI monitors, agent orchestration, language/tool diagnostics, and workspace providers.

What a plugin cannot assume

Plugins do not receive:

  • Go interfaces or tea.Msg/tea.Cmd values;
  • direct access to ui.Model, terminal emulator internals, holder protocol, or state-file JSON;
  • unrestricted access to all panes or all terminal contents;
  • arbitrary rendering outside an hrdx-owned floating frame or grid cell frame;
  • arbitrary process spawning by default;
  • guaranteed delivery of every event;
  • stable behavior from screen scraping or undocumented title strings.

A plugin uses semantic protocol objects and explicitly granted operations.


Package and manifest model

A package is a directory or archive containing a manifest, optional documentation/assets, and an executable peer or executable resolution data.

Example conceptual manifest:

{
  "schema": 1,
  "id": "example.git-tools",
  "version": "1.0.0",
  "name": "Git Tools",
  "description": "Git actions and project status for hrdx",
  "entrypoint": "git-tools",
  "protocol": {"min": 1, "max": 1},
  "activation": ["on-command", "on-workspace"],
  "contributes": {
    "commands": [
      {
        "id": "example.git-tools.open-log",
        "label": "Open Git log",
        "targets": ["workspace", "pane"]
      }
    ],
    "status": ["git.branch", "git.changed"],
    "views": ["git.changes"]
  },
  "requests": [
    "workspace.read",
    "pane.read_metadata",
    "ui.command.contribute",
    "ui.status.contribute",
    "ui.view.contribute",
    "ui.notification",
    "storage.plugin_private"
  ]
}

Manifest rules

  • id is stable and globally namespaced; display names are not identity.
  • IDs are validated before a process is launched.
  • The manifest is declarative metadata, not an arbitrary shell-script hook.
  • Unknown optional fields are ignored; unknown required fields reject activation.
  • Requested capabilities are not automatically granted.
  • Contributions are validated against the manifest schema and protocol version.
  • Entrypoints are resolved without shell evaluation; arguments use an explicit array form.
  • Package paths and executable names are not trusted as permission grants.
  • A package can be discovered, disabled, or inspected without being started.
  • Static contributions may be listed before activation, but dynamic availability comes from the peer.

Discovery locations

Use a small, deterministic search order:

  1. an instance/project plugin directory when explicitly configured;
  2. the user plugin directory under the hrdx state/config directory;
  3. platform-appropriate data directories;
  4. optionally a command-line path for development/testing.

Discovery must produce diagnostics for duplicate IDs, malformed manifests, unsupported protocols, missing entrypoints, and permission problems. Discovery must not execute arbitrary probes.

Enablement and configuration

Persist only host-owned preferences:

  • enabled/disabled state by stable plugin ID;
  • approved capability grants;
  • plugin configuration values declared by schema;
  • optional activation policy.

Keep live status, connection handles, process IDs, subscriptions, and plugin-private data outside state.json. Plugin-private storage is namespaced by plugin ID and may be workspace-scoped, but its schema is owned by the plugin.


Architecture

                 manifest/package directories
                              |
                         discovery
                              |
                       plugin registry
                              |
                 approval + capability policy
                              |
                       plugin supervisor
                              |
       local bidirectional protocol connection per peer
                              |
                       capability broker
                  /             |                \
        host requests       host events       contributions
             |                    |                  |
     UI update loop       filtered subscriptions   UI registries
             |
    workspaces / tabs / panes / PTYs / holder

Plugin registry

Owns discovered package metadata, validation diagnostics, enablement, and contribution metadata. It must be an instance-owned service rather than a package-global mutable registry, so tests and multiple models do not leak registrations into one another.

Supervisor

Owns peer process startup, local transport, handshake, liveness, graceful shutdown, crash classification, backoff, and contribution cleanup. It must expose asynchronous messages/commands to the UI rather than mutating UI state from supervisor goroutines.

The supervisor should support:

  • lazy launch;
  • explicit start/stop/restart;
  • one connection per plugin instance;
  • bounded restart attempts with backoff;
  • clean shutdown on hrdx exit;
  • no restart loops for invalid manifests or denied capabilities;
  • termination of the plugin process without touching user panes;
  • diagnostics that identify plugin ID and failure stage without leaking terminal contents.

Capability broker

Mediates every host operation. It turns requested capabilities into grants based on manifest, user policy, current scope, and host support. It must enforce scope at request time, not only at handshake.

The broker should make these distinctions explicit:

  • read versus mutate;
  • instance/global versus workspace/tab/pane scope;
  • interactive approval versus pre-approved policy;
  • host-owned operation versus plugin-owned storage;
  • one-shot invocation versus long-lived subscription.

Contribution registry

Stores validated commands, actions, status items, providers, notifications, and views keyed by plugin ID and contribution ID. Removing a plugin must atomically remove or disable all of its contributions and pending invocations.

UI adapter

Converts protocol-level semantic objects into existing UI operations. It is the only layer allowed to turn plugin requests into Bubble Tea messages/commands. The plugin system must not leak protocol types through the rest of internal/ui.


Protocol

Use a versioned, bidirectional, local-only protocol. JSON is appropriate initially because it is inspectable, cross-language, and consistent with the existing API; use explicit framing rather than relying on ambiguous line ownership if binary-safe payloads become necessary.

A JSON-RPC-like envelope is sufficient without adopting a large dependency:

{"kind":"request","id":"42","method":"command.invoke","params":{}}
{"kind":"response","id":"42","result":{}}
{"kind":"error","id":"42","error":{"code":"denied","message":"..."}}
{"kind":"event","event":"workspace.changed","params":{}}
{"kind":"cancel","id":"42"}

Required protocol features

  • request IDs and concurrent in-flight calls;
  • request deadlines or cancellation IDs;
  • explicit message kinds;
  • protocol and schema version negotiation;
  • hello/ready handshake;
  • requested capabilities and host grants;
  • structured error codes;
  • bounded message sizes;
  • progress notifications for long operations;
  • subscription filters and scopes;
  • heartbeat/liveness or transport-level closure;
  • graceful shutdown;
  • correlation of command invocation and result;
  • forward-compatible optional fields.

Handshake

  1. Host starts or connects to the peer.
  2. Host sends protocol version, host version, instance ID, and supported features.
  3. Peer identifies itself, reports implementation version, contributions, requested capabilities, and required capabilities.
  4. Host validates identity against the manifest.
  5. Broker computes grants from policy and current scope.
  6. Host sends grants and activation context.
  7. Peer responds ready or a structured activation error.
  8. Contributions become visible only after readiness.

A peer must not gain capabilities merely by declaring them. Required-but-denied capabilities produce a degraded/failed activation state with a useful diagnostic.

Event delivery

Events are filtered and scoped. The host must not send every terminal byte, raw keyboard event, or every UI event by default. General event subscriptions must not expose hrdx input data. Initial event families should include:

  • workspace/tab/pane lifecycle metadata;
  • pane title and busy-state changes;
  • selected context changes;
  • plugin/host lifecycle;
  • explicit provider refresh requests.

A plugin may receive normalized content-area input only when its view owns the focused frame and it has the separately granted ui.view.input capability. Input directed to settings, menu pickers, the hrdx footer, or other host-owned surfaces must not be forwarded as events. Events are best effort and bounded. Include a sequence number where useful, but define recovery through a fresh scoped query because subscribers can disconnect or fall behind. Never block the UI loop on a plugin.

Security boundary

The existing control socket is not a plugin capability grant. A plugin connection receives a host-issued instance/session credential or an inherited private transport endpoint, and the host maps that connection to a known plugin identity. Any transport implementation must have equivalent local ownership/permission checks on Unix and Windows.


Capability catalog

Start with names that describe stable semantic operations, not internal functions.

Read capabilities

workspace.list
workspace.read
workspace.observe
 tab.read
pane.read_metadata
pane.read_title
pane.observe_busy
pane.read_screen
selection.read
host.events.subscribe

pane.read_screen and terminal content access are sensitive and should not be bundled into ordinary workspace read access.

UI contributions

ui.command.contribute
ui.action.contribute
ui.status.contribute
ui.view.contribute
ui.view.input
ui.notification
ui.prompt

ui.prompt should be deferred until an explicit host-rendered prompt contract exists. Plugin frame input is available only through the separately granted ui.view.input capability.

Mutations

workspace.create
workspace.close
 tab.create
pane.create
pane.close
pane.send_input
pane.resize

These are high-risk. Sending input to a shell or agent is equivalent to user control and must never be implied by read access.

Process and storage

process.spawn
process.spawn_in_workspace
storage.plugin_private
storage.workspace_scoped

Process creation should be absent by default. A plugin that needs a PTY-backed operation should use a host-mediated operation with clear ownership and cleanup rules, not access the holder protocol directly.

Every capability needs documented properties: scope, sensitivity, mutability, approval requirement, persistence behavior, event visibility, and compatibility version.


Rich contribution contracts

Commands and contextual actions

A command is declared, validated, shown only when its target/context is available, and invoked through the peer protocol.

{
  "command_id": "example.git-tools.open-log",
  "context": {
    "workspace_id": "w1",
    "tab_id": "t1",
    "pane_id": "p1"
  },
  "arguments": {}
}

Results should be structured rather than raw shell text:

{
  "notifications": [],
  "actions": [
    {"type": "open_pane", "kind": "shell", "command": ["git", "log"]}
  ],
  "view": {"type": "table", "title": "Commits", "columns": [], "rows": []}
}

The host validates returned actions against grants and invokes existing pane/layout lifecycle code. A plugin cannot directly mutate a split tree.

Providers

Providers are pull-based by default. The host asks for results with a query, scope, limit, and cancellation ID. Long-lived subscriptions are explicit and bounded.

Initial provider shapes can cover:

  • workspace candidates;
  • search results;
  • diagnostics;
  • status data;
  • completion-like entries.

Do not make provider responses arbitrary UI markup. Use typed result schemas with limits and truncation rules.

Status items

A status item contains plugin ID, contribution ID, label/value, severity, optional tooltip, and action references. The host owns status layout and display-width clipping. Plugin updates are coalesced and bounded so a noisy plugin cannot starve rendering. A plugin view is different: its content layout and TUI behavior belong to the plugin inside an hrdx-owned frame.

Notifications

Notifications include severity, message, optional detail, expiry, and action IDs. They must be sanitized/truncated and should not expose terminal content unless the plugin already holds the relevant capability.

Plugin-owned views and TUI frames

Views are separate from terminal panes. A plugin view is almost entirely owned by the plugin. The plugin supplies the view's TUI implementation, internal layout, rendering, input handling, state, and actions. The plugin may open or occupy:

  • a floating frame;
  • one or more grid cell frames;
  • a view that moves between those frame types when the user or plugin requests it.

The plugin sends bounded render frames and receives normalized input events for the content area. This lets a plugin provide its own TUI implementation without receiving Bubble Tea types or running inside the hrdx process.

hrdx still owns the outer frame lifecycle, placement, z-order, sizing limits, cleanup, and mouse hit-testing. hrdx also retains ownership of the settings modal, menu pickers, and the hrdx footer input. Those host surfaces take precedence over plugin view input. A plugin cannot draw outside its assigned frame, replace the hrdx footer, or receive settings/menu/footer input through general events. It receives content-area input only while its view is focused and only when ui.view.input is granted.

This is not a terminal pane: it has no PTY, shell process, holder session, or terminal persistence identity. It is also not arbitrary rendering access to the whole hrdx UI. The plugin_views capability and feature flag gate this surface.

Plugin-provided agents

A plugin may contribute metadata or workflows around an existing agent harness, but it does not bypass startPane, removePane, holder attachment, persistence, or terminal input encoding. A future agent adapter must still produce ordinary PTY-backed panes.


Lifecycle and failure behavior

discovered -> validated -> disabled/approved
approved -> starting -> handshaking -> ready
ready -> degraded -> ready
ready -> stopping -> stopped
starting/ready -> failed

Rules:

  • Discovery and manifest errors are visible without launch attempts.
  • Denied capabilities produce a degraded/blocked status, not repeated crashes.
  • A peer crash removes its contributions and pending work, then optionally restarts under bounded policy.
  • A plugin cannot leave panes, holder sessions, or split-tree references behind when it exits.
  • Restart must recreate subscriptions and contributions from the manifest/peer handshake.
  • Host shutdown asks peers to stop, waits briefly, then terminates remaining processes using platform-specific code.
  • Existing hrdx functionality remains usable if all plugins fail.
  • Diagnostics expose lifecycle stage and structured error code, not sensitive payloads.

Lazy activation is the default. Activation triggers may be command invocation, explicit settings, workspace match, or a relevant view/provider request. Startup activation must be opt-in because plugin startup can affect latency and resource use.


Persistence and compatibility

Persist host decisions only:

{
  "plugins": {
    "example.git-tools": {
      "enabled": true,
      "grants": ["workspace.read", "ui.status.contribute"],
      "config": {"show_clean": false}
    }
  }
}

The exact location can be decided during implementation, but additive optional fields and safe zero values are required. Existing state files must load unchanged.

Compatibility policy:

  • protocol version, manifest schema, capability versions, and plugin implementation version are independent;
  • unknown optional fields are ignored;
  • unsupported optional contributions are omitted with diagnostics;
  • required unsupported capabilities block only that plugin;
  • stable IDs survive display-name changes;
  • removed plugins preserve the user's host config but do not create fake shell panes;
  • no plugin may depend on internal Go packages, Bubble Tea messages, holder frames, or state JSON.

A missing plugin should remain visibly unavailable in plugin diagnostics. Existing persisted panes continue to follow the existing harness/pane compatibility rules; plugin activation must not silently rewrite core pane identity.


Testing and observability requirements

Each layer needs deterministic tests without requiring installed third-party plugins or real shells.

Manifest/registry

  • valid and malformed manifests;
  • duplicate IDs;
  • missing entrypoints;
  • version ranges and unknown optional fields;
  • discovery ordering and platform paths;
  • enablement and grant persistence;
  • no process launch during discovery.

Protocol

  • handshake success/failure;
  • request correlation with concurrent calls;
  • cancellation and deadlines;
  • malformed/oversized messages;
  • unknown optional messages;
  • structured errors;
  • subscription filters and dropped events;
  • peer disconnect and pending-request cleanup.

Supervisor

  • startup and graceful stop;
  • crash cleanup;
  • bounded restart/backoff;
  • no restart on invalid manifest/denial;
  • contribution removal;
  • host shutdown;
  • platform transport behavior.

Capability broker

  • requested versus granted capabilities;
  • scope enforcement on every request;
  • denied read and denied mutation paths;
  • workspace/pane isolation;
  • plugin identity cannot be spoofed;
  • sensitive screen/input operations remain separately gated.

UI integration

  • commands/actions appear only when active and applicable;
  • invocation round-trips through the update loop;
  • returned actions use existing pane lifecycle;
  • status updates are bounded and clipped correctly;
  • plugin-owned view frames cannot draw outside their assigned frame;
  • settings, menu pickers, and footer input keep priority over plugin view input;
  • notifications survive plugin failure without blocking rendering;
  • cleanup removes contributions and stale selections.

End-to-end fixture

Provide a tiny test plugin implementation (preferably a Go test helper or a checked-in script fixture) that can handshake, register a command, respond to invocation, publish status, and intentionally crash. It must use synthetic paths and content only.

Observability should include plugin ID, state, protocol version, granted capability names, last transition, restart count, and sanitized error. Avoid logging terminal text, command arguments that may contain secrets, or plugin-private payloads by default.


Implementation plan: atomic, usable PRs

Each PR below is intentionally independently reviewable and leaves a usable increment. Later PRs depend only on merged earlier contracts. No PR should land a dormant abstraction that cannot be exercised by tests or a small fixture.

Discussion 0 — Document and freeze the public design boundary

Deliverable: protocol/manifest/capability ADR plus a minimal public schema document and compatibility rules.

  • Record that plugins are external peers, not embedded Go plugins or virtual panes.
  • Define stable IDs, version fields, capability naming, scope terminology, and error vocabulary.
  • Define package discovery roots and host-owned persistence policy.
  • Add schema fixtures for one valid and several invalid manifests.
  • Add no runtime behavior yet.

After this discussion: maintainers and plugin authors know the planned contract. No user-facing behavior changes yet.

PR 1 — Manifest types, validation, and discovery (no feature flag)

Deliverable: a pure internal/plugin (or equivalent) package that parses and validates manifests and discovers packages without launching them.

  • Implement manifest structs with additive JSON behavior.
  • Validate IDs, versions, entrypoints, contributions, requested capabilities, and protocol ranges.
  • Implement deterministic platform-aware discovery and duplicate diagnostics.
  • Add registry tests and a small diagnostic/status representation.

After deployment, users can: run a plugin inventory/diagnostic command or view to see discovered plugins, versions, validation errors, and unavailable plugins. No plugin processes start yet.

PR 2 — Host plugin preferences and settings visibility (no feature flag)

Deliverable: persist enabled state, grants, and declared configuration without breaking old state files.

  • Add optional plugin preferences to internal/state.
  • Add snapshot/restore coverage in internal/ui/persist.go.
  • Expose a read-only registry/status view or settings list.
  • Keep live connections and plugin data out of core state.

After deployment, users can: enable or disable discovered plugins and keep those choices after restarting hrdx. Disabled plugins do not start.

PR 3 — Versioned protocol package and in-memory transport tests (no feature flag)

Deliverable: protocol envelopes, handshake structs, errors, request IDs, cancellation, and subscription message types.

  • Keep transport-independent types in a package that external clients can reproduce.
  • Implement framing/codec limits and strict message validation.
  • Add deterministic in-memory duplex tests for concurrent requests and disconnect cleanup.
  • Do not connect it to UI yet.

After deployment, users can: run a protocol-compatible example plugin that connects over the test transport and reports its handshake status. Plugin functionality is not exposed in the main UI yet.

PR 4 — Local transport on Unix and Windows (no feature flag)

Deliverable: authenticated/local-only transport adapters using Unix sockets on Unix and named pipes or equivalent local transport on Windows.

  • Reuse standard-library facilities where possible.
  • Enforce endpoint ownership/permissions and instance-scoped credentials.
  • Add platform-specific tests and build coverage.
  • Keep the existing public control socket separate.

After deployment, users can: run an enabled plugin as a real local subprocess and see it connect on Unix and Windows. The plugin still has no hrdx data access.

PR 5 — Plugin supervisor and lifecycle state machine (gated by experimental.plugins)

Deliverable: launch, handshake, ready/degraded/failed/stopped transitions, graceful shutdown, crash detection, and bounded restart.

  • Start peers with explicit argv/env/cwd; never shell-evaluate manifest strings.
  • Associate every process and connection with one plugin ID/instance.
  • Expose lifecycle messages to the host/UI; do not mutate UI from supervisor goroutines.
  • Add fake-process/fixture tests for success, crash, denial, and restart policy.

After deployment, users can: start, stop, and restart enabled plugins. A crashed plugin is logged and removed without stopping hrdx, existing panes, or holder sessions.

PR 6 — Capability broker and policy enforcement (gated by experimental.plugins)

Deliverable: requested/granted capability negotiation with scope checks on every operation.

  • Implement default-deny policy.
  • Add grant persistence from PR 3.
  • Represent workspace/tab/pane scope explicitly.
  • Return stable denied/invalid-scope errors.
  • Add security tests for spoofed IDs, cross-workspace access, and mutation denial.

After deployment, users can: run plugins with explicit, visible grants. A plugin that has no grant cannot read or change hrdx data, even if it requests that access.

PR 7 — Read-only host query API (gated by experimental.plugins)

Deliverable: brokered queries for workspace/tab/pane metadata and selected context.

  • Use semantic DTOs, not live UI structs.
  • Route requests through the Bubble Tea update loop via buffered replies.
  • Add bounded results and cancellation.
  • Implement workspace.read, pane.read_metadata, and selected-context reads first.

After deployment, users can: use a plugin that shows read-only workspace, tab, and pane information without allowing the plugin to control terminals or layouts.

PR 8 — Filtered event subscriptions (gated by experimental.plugins)

Deliverable: scoped lifecycle, selection, title, and busy-state subscriptions.

  • Add explicit subscription registration/removal.
  • Filter by event family and workspace/pane scope.
  • Keep delivery best effort and non-blocking.
  • Document recovery by query after missed events.
  • Add sequence/reconnect behavior if the protocol contract requires it.

After deployment, users can: see plugin data update when selected workspaces, tabs, panes, titles, or busy states change. Missed events can be recovered with a fresh snapshot.

PR 9 — Command and contextual-action contributions (gated by experimental.plugins)

Deliverable: manifest-declared commands/actions, host registration, menu/picker integration, invocation context, and structured result responses.

  • Validate contributions at handshake.
  • Add plugin-owned command IDs to existing menu/action routing without exposing arbitrary callbacks.
  • Ensure stale contributions disappear on disconnect.
  • Add command invocation cancellation and failure notifications.

After deployment, users can: run plugin-provided commands and contextual actions from hrdx menus. The plugin receives structured workspace and pane context instead of scraping the UI.

PR 10 — Host-rendered notifications and status items (gated by experimental.plugins)

Deliverable: bounded notifications and status contributions.

  • Define severity, expiry, width, truncation, and action-reference rules.
  • Coalesce noisy updates and prevent render-loop blocking.
  • Keep rendering host-owned and ANSI-safe.
  • Add fixture plugin showing branch/status-like data.

After deployment, users can: see plugin-provided status indicators and notifications for information such as CI state, Git status, or project health.

PR 11 — Plugin-owned TUI views (gated by experimental.plugins and experimental.plugin_views)

Deliverable: plugin-owned TUI implementations hosted in hrdx floating frames or grid cell frames.

  • Add a separate view lifecycle, not a pane kind.
  • Define bounded render frames, frame placement, floating/grid-cell ownership, and refresh rules.
  • Let plugins own view layout, rendering, internal state, and content input.
  • Let hrdx own frame placement, z-order, sizing limits, cleanup, settings modal, menu pickers, and footer input.
  • Add ui.view.input grants and route only content-area input to the plugin.
  • Ensure plugins cannot draw outside their frame or receive host-owned input through view input or general event subscriptions.
  • Add keyboard/mouse hit-testing, frame clipping, and disconnect cleanup tests.

After deployment, users can: open plugin-owned TUI panels in floating frames or grid cells. The plugin controls the panel's content and behavior, while hrdx retains control of the surrounding frame, settings, menus, and footer.

PR 12 — Plugin private storage (gated by experimental.plugins)

Deliverable: namespaced, host-mediated plugin storage with optional workspace scope.

  • Define size/value limits and atomic writes.
  • Keep data separate from state.json and inaccessible to other plugins by default.
  • Add migration/version metadata owned by the plugin.
  • Test path safety and workspace isolation.

After deployment, users can: use plugins that save their own indexes and preferences across restarts without writing into or corrupting hrdx workspace state.

PR 13 — Structured host actions using existing pane lifecycle (gated by experimental.plugins and experimental.plugin_pane_actions)

Deliverable: a small allowlisted result/action vocabulary for opening existing PTY-backed panes, notifications, and selecting existing resources.

  • Route pane creation through existing pane.create/startPane/persistence paths.
  • Do not expose split-tree mutation or holder protocol.
  • Require explicit mutation grants and user-visible confirmation where appropriate.
  • Test cleanup on plugin crash and action cancellation.

After deployment, users can: use an approved plugin action to open a normal hrdx shell or agent pane. The pane still uses the normal PTY, layout, holder, and persistence paths.

PR 14 — Plugin management UI and diagnostics (gated by experimental.plugins)

Deliverable: a user-facing plugin screen for discovery, enablement, grants, lifecycle, errors, and restart.

  • Show manifest identity, version, contributions, requested/granted capabilities, and current state.
  • Make sensitive grants explicit.
  • Surface malformed/blocked plugins without attempting activation.
  • Ensure the UI remains usable if the supervisor is unhealthy.

After deployment, users can: inspect, enable, disable, start, stop, restart, and troubleshoot plugins from hrdx controls instead of editing files or killing processes manually.

PR 15 — Workspace-scoped activation and provider APIs (gated by experimental.plugins)

Deliverable: declarative workspace matching and pull-based providers.

  • Match only local declarative markers/path patterns; no arbitrary startup shell probes.
  • Add provider request limits, cancellation, and stale-result handling.
  • Scope activation and grants to the relevant workspace where possible.
  • Add an example workspace provider fixture.

After deployment, users can: have relevant plugins activate for matching projects and provide search, diagnostic, or workspace data when requested.

PR 16 — Hardening, migration, and release documentation (flags remain off by default)

Deliverable: production readiness across platforms and user documentation.

  • Run full tests, vet, race tests, and Windows/macOS build coverage.
  • Audit process cleanup, socket permissions, message limits, secrets in logs, and restart loops.
  • Document package installation, trust model, grants, protocol compatibility, troubleshooting, and authoring.
  • Add a compatibility matrix and a reference fixture plugin.
  • Preserve existing API, harness, holder, state, and pane behavior in regression tests.

After deployment, users can: install and use documented third-party plugins across supported platforms with clear compatibility, diagnostics, cleanup, and recovery behavior.


Delivery-plan revision after comparing Zot

The contract and design boundary are delivered as a discussion thread, not a code PR. The detailed implementation sequence therefore contains 16 PRs, plus that discussion thread. Zot changes the recommended critical path. The first implementation should use a supervised child process over stdin/stdout with newline-delimited JSON. Do not make Unix sockets and Windows named pipes a prerequisite for the first usable plugin.

Zot provides a proven baseline for the mechanical parts:

  • project-local-over-global manifest discovery;
  • direct executable plus argv launch;
  • hellohello_ack → registrations → ready;
  • per-plugin stderr logs;
  • fail-soft crashes;
  • bounded shutdown;
  • explicit development override and reload;
  • plugin-owned TUI views hosted in constrained hrdx frames;

hrdx should retain the parts Zot does not provide:

  • default-deny, scoped capabilities;
  • semantic workspace/tab/pane APIs;
  • Bubble Tea update-loop ownership;
  • existing PTY, holder, layout, and persistence lifecycle;
  • snapshot/resync after missed events;
  • no virtual panes or direct holder access.

Revised first increments

The early PRs should be reordered to make the system usable sooner:

  1. Freeze contract and limits. Define NDJSON-over-stdio for v1, frame/message limits, request and shutdown timeouts, hello/hello_ack/ready, discovery precedence, trust assumptions, and compatibility rules.
  2. Manifest discovery plus plugins list/plugins doctor. Deliver inventory and diagnostics without starting processes. Include enabled state, relative executable resolution, duplicate/shadow reporting, and project-over-global precedence.
  3. Stdio protocol plus fixture plugin. Implement typed frames, IDs, errors, bounded readers, and a tiny external fixture that registers one command and emits one notification.
  4. Supervisor, logs, shutdown, and reload. Add process isolation, handshake failure handling, restart policy, explicit --plugin/--ext development loading, and reload. At this point a developer can run and iterate on a real plugin.
  5. Preferences and enablement. Persist enabled state, grants, and plugin configuration additively; keep live registrations and private runtime data out of core state.
  6. Capability broker. Add explicit host grants and per-request scope enforcement before exposing hrdx data or mutations.
  7. Read-only metadata and scoped subscriptions. Add semantic workspace/tab/pane DTOs, UI-loop routing, bounded subscriptions, and snapshot/resync.
  8. Commands/actions and notifications. Integrate with existing menu infrastructure using namespaced IDs and structured results.
  9. Plugin-owned TUI views. Let plugins render and control their own content inside hrdx floating frames or grid cells, while hrdx retains frame, settings, menu, and footer ownership.
  10. Private storage and PTY-backed actions. Add namespaced storage and an allowlisted action vocabulary routed through existing pane lifecycle code.
  11. Management UI, SDK/examples, and hardening. Complete operational controls, reference clients, compatibility tests, platform checks, and documentation.

The original PRs can still be used as smaller review units. Specifically, the old transport-core and platform-transport PRs become one stdio transport track, while socket/named-pipe transport moves to a later optional phase for independently managed or multi-client plugins.

Experimental feature flags

Yes, add a small host-owned feature-flag dictionary early, but keep it narrow and opt-in.

Recommended shape:

{
  "experimental": {
    "plugins": false,
    "plugin_views": false,
    "plugin_pane_actions": false
  }
}

Rules:

  • Store it with hrdx's existing user preferences/state rather than in plugin manifests.
  • All flags are false by default. They remain off until maintainers explicitly decide to enable a flag by default in a later release.
  • Users may opt in earlier by changing the settings value.
  • Unknown flag names are ignored, so old hrdx versions remain compatible.
  • Missing flags default to false.
  • Plugins cannot turn flags on themselves.
  • Each flag gates one complete user-visible surface, not arbitrary internal code paths.
  • Stable, low-risk foundations such as manifest discovery and diagnostics may ship without a flag.
  • Process execution, external plugin access, views, pane actions, and other risky surfaces should remain disabled until their PR is complete and explicitly enabled.
  • A merged PR does not turn its feature on. The corresponding flag must be enabled by the user or by a later maintainer decision.
  • Show enabled experimental features in diagnostics so users can explain their setup.
  • When a feature becomes stable, keep the flag as a harmless compatibility alias or migrate it safely; do not silently change a user's behavior during an upgrade.

This is useful for delivering the PRs incrementally without surprising existing users. It is not a security boundary: a plugin remains a trusted executable with the operating-system permissions of the user. Capabilities and grants must still enforce access.

New transport boundary

The first plugin connection should be owned by the supervisor and backed by the child process's stdio. A future socket transport should be added only for a concrete requirement such as a plugin daemon surviving hrdx restarts, multiple hrdx instances, or independently managed services. It must preserve the same protocol and capability broker rather than creating a second plugin API.


Dependency graph and parallel work

The critical path is:

PR 1 -> PR 2 -> PR 3
          \      \
           -> PR 4 -> PR 5 -> PR 6
                                      |
                         PR 7 -> PR 8 -> PR 9
                                      |       \
                                      -> PR 10 -> PR 11
                                      |       \
                                      -> PR 12  -> PR 13 -> PR 14
                                      |
                                      -> PR 15 -> PR 16

After PR 3, supervisor work (PR 5), preference work (PR 2), and fixture tooling can proceed in parallel. After PR 6, read queries (PR 7), event subscriptions (PR 8), and storage plumbing (PR 12) are mostly independent. Rich UI contributions should wait until the protocol, supervisor, and capability enforcement are real.

Each PR should include:

  • a narrow public contract;
  • focused unit tests;
  • a fixture or CLI demonstration when relevant;
  • failure-path behavior;
  • no unrelated refactors;
  • documentation for any user-visible behavior;
  • a migration note if state or protocol changes.

Explicitly deferred features

These are compatible with option 4 but should not be allowed to destabilize the first platform:

  • embedded native Go plugins;
  • arbitrary WASM execution;
  • arbitrary rendering outside plugin-owned floating/grid frames;
  • plugin-owned virtual terminal panes;
  • unrestricted process spawning;
  • direct holder access;
  • remote/network plugin daemons;
  • plugin marketplace, signing infrastructure, or automatic downloads;
  • durable event replay as a replacement for query-based recovery;
  • plugins mutating core state files;
  • automatic approval of sensitive capabilities.

They can be considered only after real integrations demonstrate a concrete need and the existing capability model can express their risks.


Success criteria

Option 4 is complete enough to ship when all of the following are true:

  • A third party can discover, validate, install, enable, and disable a plugin without modifying hrdx.
  • A plugin can connect in any practical implementation language through documented local IPC.
  • Handshake, grants, scope, cancellation, failure, restart, and shutdown are deterministic.
  • A plugin can read selected workspace/pane metadata, subscribe to filtered changes, contribute commands/status/views, retain private state, and run a TUI inside an approved floating or grid-cell frame.
  • Plugin actions use existing hrdx pane/layout/PTY lifecycle rather than bypassing it.
  • A crashed or maliciously noisy plugin cannot block rendering, corrupt state, or leave orphaned sessions behind.
  • Existing hrdx users can run with no plugins and see no regression in startup, terminal behavior, persistence, or platform support.
  • The protocol and manifest are stable enough that plugin authors do not need internal hrdx knowledge.

The core architectural rule is:

Option 4 is a package-and-protocol platform: declarative metadata says what a plugin contributes, a supervised external peer performs the work, and a capability broker controls every interaction with hrdx.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment