Status: Accepted (Updated 2026-01-04) Date: 2026-01-04 Deciders: QNTX Core Team
Domain plugins need configuration for:
Requirements:
am.tomlPlugins are configured via a single [plugin] section in am.toml:
[plugin]
enabled = ["code"] # Whitelist of plugins to load
paths = ["~/.qntx/plugins", "./plugins"] # Where to search for binaries
Key principles:
enabled list means minimal core modeenabled listQNTX searches for plugin binaries using common naming conventions:
~/.qntx/plugins/qntx-code-plugin # Preferred naming
~/.qntx/plugins/qntx-code # Alternative
~/.qntx/plugins/code # Fallback
./plugins/qntx-code-plugin # Project-level plugins
Discovery algorithm:
enabled list (e.g., "code")paths for binaries matching:
qntx-{name}-pluginqntx-{name}{name}Plugin-specific settings remain in am.toml under domain namespace:
# Core QNTX configuration
[database]
path = "qntx.db"
[server]
port = 877
[pulse]
workers = 4
# Plugin configuration
[plugin]
enabled = ["code"]
paths = ["~/.qntx/plugins"]
# Code plugin specific settings
[code.gopls]
enabled = true
workspace_root = "."
[code.github]
# API token preferably from environment: QNTX_CODE_GITHUB_TOKEN
Plugins receive Config interface via ServiceRegistry:
func (p *Plugin) Initialize(ctx context.Context, services ServiceRegistry) error {
config := services.Config("code") // Gets [code.*] section from am.toml
// Provide sensible defaults
workspace := config.GetString("gopls.workspace_root")
if workspace == "" {
workspace = "." // Default to current directory
}
// Optional features degrade gracefully
apiToken := config.GetString("github.api_token")
if apiToken == "" {
p.logger.Warn("GitHub API token not configured, PR integration disabled")
// Plugin still initializes, feature disabled
}
}
Sensitive values should prefer environment variables:
# .env or shell
export QNTX_CODE_GITHUB_TOKEN="ghp_..."
export QNTX_DATABASE_PATH="custom.db"
Environment variables follow pattern: QNTX_{DOMAIN}_{KEY}
Configuration precedence:
am.toml values# am.toml - minimal QNTX
[database]
path = "qntx.db"
[server]
port = 877
# No [plugin] section = no plugins loaded
QNTX runs with only:
[plugin]
enabled = ["code"]
paths = ["~/.qntx/plugins", "./plugins"]
[code.gopls]
enabled = true
workspace_root = "."
[plugin]
enabled = ["code", "finance", "biotech"]
paths = ["~/.qntx/plugins"]
[code.gopls]
workspace_root = "/workspace/main-repo"
[finance]
api_key = "${FINANCE_API_KEY}"
[biotech.ncbi]
api_key = "${NCBI_API_KEY}"
email = "researcher@example.com"
✅ Minimal by default: No plugins loaded unless explicitly configured
✅ Simple discovery: Just drop binary in ~/.qntx/plugins/ and add to enabled list
✅ Centralized config: All configuration in one am.toml file
✅ Flexible paths: Support both user-level (~/.qntx/plugins) and project-level (./plugins)
✅ Optional: QNTX works without any plugins (minimal core mode)
✅ Standard naming: Common conventions make plugin binaries discoverable
⚠️ Manual installation: Users must download/build plugin binaries ⚠️ Path management: Users must ensure binaries are in configured paths ⚠️ No version management: No automatic plugin updates (manual for now)
// am/am.go
type Config struct {
Plugin PluginConfig `mapstructure:"plugin"`
// ... other config sections
}
type PluginConfig struct {
Enabled []string `mapstructure:"enabled"` // Whitelist of plugins
Paths []string `mapstructure:"paths"` // Search paths
}
// plugin/grpc/loader.go
func LoadPluginsFromConfig(ctx context.Context, cfg *am.Config, logger *zap.SugaredLogger) (*PluginManager, error) {
manager := NewPluginManager(logger)
if len(cfg.Plugin.Enabled) == 0 {
logger.Infow("No plugins enabled - QNTX running in minimal core mode")
return manager, nil
}
// Discover plugins from configured paths
for _, pluginName := range cfg.Plugin.Enabled {
pluginConfig, err := discoverPlugin(pluginName, cfg.Plugin.Paths, logger)
if err != nil {
logger.Warnw("Failed to discover plugin", "plugin", pluginName, "error", err)
continue
}
// Load plugin via gRPC
if err := manager.LoadPlugins(ctx, []PluginConfig{pluginConfig}); err != nil {
return nil, err
}
}
return manager, nil
}
Plugins should use these naming conventions for discoverability:
| Pattern | Example | Priority |
|---|---|---|
qntx-{domain}-plugin | qntx-code-plugin | Preferred |
qntx-{domain} | qntx-code | Alternative |
{domain} | code | Fallback |
All binaries must be:
chmod +x)Before Phase 3:
// main.go - hardcoded
codePlugin := qntxcode.NewPlugin()
registry.Register(codePlugin)
After Phase 3:
# am.toml
[plugin]
enabled = ["code"]
paths = ["~/.qntx/plugins"]
// main.go - discovery
manager, _ := grpc.LoadPluginsFromConfig(ctx, cfg, logger)
for _, plugin := range manager.GetAllPlugins() {
registry.Register(plugin)
}
Potential future enhancements:
$ qntx plugin install code
Downloading qntx-code-plugin v0.2.0...
Installing to ~/.qntx/plugins/qntx-code-plugin
Added to am.toml: plugin.enabled = ["code"]
$ qntx plugin list
code v0.2.0 [enabled] Software development domain
finance v0.1.0 [available] Financial analysis domain
biotech - [available] Bioinformatics domain
Rejected: File proliferation, unclear which plugins are enabled, harder to manage
Rejected: Too complex for Phase 3, adds external dependency
Rejected: Security risk (auto-loading unknown binaries), against minimal core principle
Rejected: Platform-specific, fragile across Go versions, build complexity