Skip to content

Plugin lifecycle ​

Planned lifecycle API (installed → enabled → disabled → uninstalled, install/enable/disable/uninstall endpoints, version replacement) per openspec/changes/keel-plugin-core/specs/plugin-system/spec.md ("生命周期"). The assembly mechanics it builds on are implemented in internal/agentext and internal/servicegraph and are cited below.

1. The state machine ​

            install                enable                  disable
  (absent) ─────────▶ installed ─────────▶ enabled ◀────────────┐
                        │   ▲                │                   │
                        │   │   upgrade      │  disable          │
                        │   └──── (same id,   ▼                   │
                        │        new version) disabled ──────────┘
                        │                        │
                        ▼                        ▼
                   uninstalled ◀───────────────  uninstall
  • install validates the manifest, the engines.keel gate and the package hash, stores the package in the managed directory and writes an audit row (Manifest).
  • uninstalled removes the managed directory; it is refused while the plugin is enabled and in use.
  • upgrade replaces the version of the same id — one enabled version per id at any time.

Enable and disable are assembly operations, not flag flips. Enable runs the plugin through dependency-ordered activation; disable must remove every side effect it registered. The implemented host does exactly this for its in-process plugins, and the planned lifecycle reuses it:

PromiseMechanism (implemented)Citation
dependency-ordered activationiterative activation loop; dependents activate once their requirements are providedinternal/agentext/plugin.go:257
missing dependency ⇒ not loaded, visiblystate PENDING + waiting for: <ids> diagnosticinternal/agentext/plugin.go:291
disable removes toolsthe published tool set is recomputed after every assemblyinternal/agentext/plugin.go:438
disable removes subscriptions/triggersunsubs and triggerStops are released in reverse orderinternal/agentext/plugin.go:387
disable runs declared cleanups exactly onceeffectsRun bookkeeping prevents double execution on partial unloadinternal/agentext/plugin.go:396
a failed activation rolls back its own effectsfailure path calls cleanup before returninginternal/agentext/plugin.go:317
every change is visiblegeneration increments once per assembly; snapshot reports states + diagnosticsinternal/agentext/plugin.go:209, :505

The spec's acceptance scenario is the same statement in product terms: enable a plugin that provides tool x and a webhook trigger, disable it, and the model no longer has x, the trigger no longer fires, the inventory shows disabled, and nothing errors (spec: 停用即撤销).

2. Dependency rules ​

  • requires names plugins or services. A requirement is satisfied when something active provides it; a plugin that cannot be satisfied stays disabled with a named reason. It is never partially loaded (spec: 依赖缺失不装载; mechanism: internal/agentext/plugin.go:291).
  • Tools are a global namespace: two enabled plugins may not provide the same tool name. The second one fails closed with a diagnostic, whichever order they are assembled in (internal/agentext/plugin.go:236, :454).
  • Hook events must belong to the closed set; an unknown event is an error at subscription time, never a silent no-op (internal/agentext/hooks.go:249).

3. Hot update without disturbing in-flight work ​

Plugins are process-local registration, so an update is a re-assembly. Two levels of protection keep it from disturbing work in progress:

  1. Product level (today). Runtime.Apply unloads the previous assembly first, then activates the new one and increments the generation once. Hook callbacks and trigger fires that reference removed plugins fail visibly in their own counters; committed runs are untouched because the run's own registrations live in its transcript, not in the plugin table (internal/agentext/plugin.go:186, internal/agentext/hooks.go:399).
  2. Service level (where plugins become services). The planned unification routes official plugins through the service graph, which adds generations with in-flight references: RETIRING instances accept no new runs and are unloaded in reverse dependency order once their references drain, or forcibly after the retire deadline — and a forced retire marks the holder's lease broken so the run fails with a drain-expired diagnostic instead of writing against a dead instance (internal/servicegraph/lifecycle.go:21, internal/servicegraph/engine.go:270, :303, :650).

4. What happens to work a plugin started ​

Work itemOn disable
tool provided by the pluginremoved from the model's tool set at the next assembly
hook subscriptionreleased immediately (reverse order)
triggerunregistered; no further fires; its existing audit rows remain
declared effect (timer, listener, connection)Stop(ctx) runs with a 2s bounded context; failures are logged, never fatal
sidecar processclose on the protocol, then stdin close → grace → SIGTERM → SIGKILL on the process group
sub-agent sessions the plugin spawnedkeep running — they are sessions in the ledger, not plugin state

(internal/agentext/plugin.go:387 for effects; sidecar close path internal/gateway/stdio.go:228.)

5. Audit ​

Every lifecycle action — install, enable, disable, uninstall, update — and every permission refusal must record time, operator, plugin id, version, action, result, and be queryable through GET /v1/plugins/audit (spec: 审计). The audit lane reuses the same discipline as the trigger audit today: bounded records, ordered by time, with the actor and the outcome (internal/agentext/triggers.go:86 for the existing audit-entry shape).

6. Lifecycle in the UI ​

The current settings screen already renders the assembled surface — generation, per-plugin state chips (active/pending/failed/stopped), provided tools, hooks, triggers and diagnostics (web/src/screens/settings.tsx:1005, :1148). The planned lifecycle adds rows for installed/disabled packages plus enable/disable/uninstall/update actions and an audit view — same table, more states.

Apache-2.0 (MIT notices kept for files moved from pigo)