ADR-022: Python as Plugin-Provided Service

Status

Implemented

Context

Python execution was hardcoded to a single plugin named "python". The watcher engine checked if p == "python" at boot to register the "py" glyph type, then executed Python code via HTTP POST to /api/python/execute. This coupled the execution path to a specific plugin name and used HTTP instead of gRPC — unlike every other plugin-provided service (LLM, search, embedding).

The real value of Python integration is the ecosystem — the libraries available in the runtime, not the language itself. A single monolithic Python plugin that bundles all packages is the wrong abstraction. Different domains need different Python environments: bioinformatics needs biopython/dnachisel/numpy, data analysis needs pandas/scipy, ML needs torch/transformers. These are separate concerns that belong in separate plugins.

Decision

PythonService gRPC

Add PythonService following the established provider pattern (ADR-014, ADR-015, ADR-017). A plugin declares python_provider = true in InitializeResponse and implements PythonService.Execute via gRPC. QNTX discovers the capability dynamically and routes "py" glyph execution to the provider — no name checks.

Protocol

python.proto:

service PythonService {
  rpc Execute(PythonExecuteRequest) returns (PythonExecuteResponse);
}

message PythonExecuteRequest {
  string code = 1;
  string glyph_id = 2;
  bytes upstream_attestation = 3;
}

message PythonExecuteResponse {
  bool success = 1;
  string output = 2;
  string error = 3;
  bytes result = 4;
}

domain.proto: bool python_provider = 9 on InitializeResponse.

Core side

Plugin side

Specialized Python plugins via Nix

The Rust binary (qntx-python-plugin) is the chassis — identical code for all Python plugins. The Nix flake is the configuration surface. Each specialized plugin is a separate Nix derivation that:

  1. Fetches qntx-python-plugin source from the QNTX repo
  2. Builds it against a curated python313.withPackages environment
  3. Renames the binary to qntx-{name}-plugin
  4. Declares python_provider: true with its own name

Example — a bioinformatics plugin:

pythonWithPackages = python313.withPackages (ps: with ps; [
  dnachisel numpy biopython
]);

A different plugin for data analysis:

pythonWithPackages = python313.withPackages (ps: with ps; [
  pandas scipy matplotlib
]);

Same Rust binary, same gRPC protocol, different Python environments. Each runs as a separate process with its own port. QNTX sees them as independent plugins.

Development workflow

Consequences

Open question: multiple python providers

AddPythonProvider is last-writer-wins — a single pythonClient on the server, a single PythonExecutor on the watcher engine. The Nix specialization pattern enables multiple Python plugins with different library sets, but the "py" glyph can only route to one provider at a time. If multiple plugins declare python_provider = true, which one handles execution?

Options not yet decided:

This doesn't need resolution now (there's one provider), but will when a second Python plugin appears.