Plugin system

Updated Jun 22, 2026

The weaver-plugins crate provides the plugin orchestration layer that enables weaverd to delegate specialist tasks to external tools running in sandboxed processes.

Plugin categories

Plugins are categorized as either sensors or actuators:

  • Sensors provide data to the intelligence engine (e.g. jedi for Python static analysis). They produce structured JSON output.
  • Actuators perform actions on the codebase (e.g. rope for Python refactoring, srgn for structural rewriting). They produce unified diffs.

Plugin manifest

Each plugin is described by a manifest containing:

Field Description
name Unique plugin identifier (e.g. rope).
version Plugin version string.
kind sensor or actuator.
languages List of supported languages (case-insensitive).
executable Absolute path to the plugin binary.
args Default arguments passed to the executable (optional).
timeout_secs Maximum execution time in seconds (default: 30).

IPC protocol

Plugins communicate with the broker via a single-line JSONL exchange over standard I/O:

  1. The broker writes one JSONL request line to the plugin's stdin and closes stdin.
  2. The plugin writes one JSONL response line to stdout and exits.
  3. Plugin stderr is captured for diagnostic logging but is not part of the protocol.

File content is passed in-band as part of the request body, so sandboxed plugins do not need filesystem access.

Plugin registry

The daemon maintains a PluginRegistry that stores validated plugin manifests keyed by name. Plugins can be looked up by name, kind, language, or a combination thereof (e.g. "find all actuator plugins for Python").

For the current actuator rollout, weaverd registers:

  • rope
  • kind: actuator
  • language: python
  • capabilities: ["rename-symbol"]
  • executable: /usr/bin/weaver-plugin-rope (or WEAVER_ROPE_PLUGIN_PATH)
  • timeout: 30s
  • rust-analyzer
  • kind: actuator
  • language: rust
  • capabilities: ["rename-symbol"]
  • executable: /usr/bin/weaver-plugin-rust-analyzer (or WEAVER_RUST_ANALYZER_PLUGIN_PATH)
  • timeout: 60s

Plugin capabilities

Actuator plugins declare the capabilities they support in their manifest. The daemon uses these declarations to route operations to the correct plugin based on both the requested capability and the target language.

Capability identifiers

The following capability identifiers are defined:

Table: Code transformation capabilities.

Identifier Description
rename-symbol Rename a symbol across a codebase.
extricate-symbol Move a symbol to a different module or file.
extract-method Extract a code region into a new function or method.
replace-body Replace the body of a function or method.
extract-predicate Extract a conditional expression into a predicate.

The rename-symbol capability contract

The rename-symbol capability is the first fully specified contract. Plugins that declare this capability must accept requests containing three required fields in the arguments map. The built-in rope and rust-analyzer rename plugins are validated against the same shared contract fixtures. Request and response checks therefore stay aligned across Python and Rust rename flows.

Table: Required fields for rename-symbol requests.

Field Type Description
uri string File URI of the symbol to rename.
position string Internal position value used by the daemon-to-plugin request. The current built-in plugins receive a UTF-8 byte offset converted from CLI --position.
new_name string The new name for the symbol (must be non-empty).

Successful responses must contain a Diff output with a unified diff patch. If a plugin reports success with any other output shape, Weaver refuses the response, exits with status 1, and makes no filesystem changes. Failed responses may include diagnostics with an optional reason_code field.

Contract versioning

Each capability contract carries a version (major.minor). Contracts with the same major version are considered compatible. The current rename-symbol contract version is 1.0.

Refusal reason codes

When a plugin cannot perform a requested operation, it returns a failure response with diagnostics. Each diagnostic may include a reason_code for programmatic matching:

Table: Refusal reason codes for plugin diagnostics.

Reason code Meaning
symbol_not_found The target symbol could not be located.
macro_generated The symbol is generated by a macro.
ambiguous_references Multiple candidate symbols match the position.
unsupported_language The plugin does not support the target language.
incomplete_payload Required fields are missing from the request.
name_conflict The new name conflicts with an existing symbol.
operation_not_supported The plugin does not support the requested operation.

Reason codes are stable identifiers intended for automation. They appear in the JSON diagnostic payload alongside the human-readable message field.

Manifest capability declarations

Actuator plugins declare capabilities in their manifest:

name = "rope"
version = "1.0.0"
kind = "actuator"
languages = ["python"]
executable = "/usr/bin/weaver-plugin-rope"
capabilities = ["rename-symbol"]

Sensor plugins must not declare any capabilities. The registry validates this constraint during registration and rejects manifests that violate it.

The daemon uses capability declarations to select plugins. For example, when routing a rename-symbol request for Python, the daemon queries the registry for actuator plugins that declare rename-symbol and support the python language. For act refactor, operators must still pass --provider explicitly, using rope for Python rename flows or rust-analyzer for Rust rename flows. The daemon refuses deterministically for unsupported languages, unknown providers, and explicit provider/language mismatches.

Safety harness integration

Actuator plugin output (unified diffs) flows through the same Double-Lock safety harness used by act apply-patch. Changes are validated by both the syntactic (Tree-sitter) and semantic (LSP) locks before any filesystem write is committed. If verification fails, the filesystem is left untouched and a structured error is returned to the caller.