Slim daemon builds (Cargo feature-gates)

Phase 93.12.a (2026-05-15) introduced Cargo feature-gates for canonical plugin crates so operators targeting embedded or mobile (Android Flutter FFI, slim Docker images) can ship a daemon binary without the optional plugin crates in its compile graph.

Available features

FeatureDefaultDrops crate
plugin-telegram✅ onnexo-plugin-telegram
plugin-whatsapp✅ onnexo-plugin-whatsapp
plugin-browseroff(no-op placeholder; browser already has no Cargo dep)

email is NOT feature-gated — structurally in-process by design (Phase 93.11 audit, bucket D). Autonomous worker + EmailToolContext + /metrics rendering all hold Arc<EmailPlugin> in-process. No subprocess driver today.

whatsapp gate (93.12.c.1 + 93.12.c.2, shipped)

Both halves shipped — slim daemon binary can be built without nexo-plugin-whatsapp in its compile graph:

cargo build --release --bin nexo --no-default-features
cargo tree --no-default-features -i nexo-plugin-whatsapp
# expected: error: package ID specification ... did not match any packages

Gated sites:

CrateSiteDetail
src/main.rsRuntimeHealth.wa_pairingtyped BTreeMap<String, SharedPairingState> field
src/main.rsspawn_whatsapp_pairing_state_subscriberbroker subscriber fn
src/main.rsspawn_whatsapp_typing_presence_subscribertyping-presence broker bridge fn
src/main.rsbuild_known_pairing_registryWhatsappPairingAdapter::new
src/main.rsadmin pairing trigger mapWhatsappPairingTrigger::from_configs
src/main.rsinstance loopwa_pairing + wa_tunnel_cfg population
src/main.rssubscriber spawn blockspawn_whatsapp_pairing_state_subscriber call
src/main.rstunnel auto-open/whatsapp/pair Cloudflare quick tunnel
src/main.rstool fallback (boot)register_whatsapp_tools
src/main.rstool fallback (hot-spawn)register_whatsapp_tools
src/main.rsHTTP handler/whatsapp/* route dispatcher
crates/setup/src/writer.rspairing flowsession::pair_once + helpers + dual-shape wipe_channel_session
crates/setup/src/admin_bootstrap.rsadmin RPCwith_wa_bot_handle + outbound translator
crates/setup/src/admin_adapters.rsoutbound translatorWhatsAppTranslator struct + impl + tests
crates/setup/tests/channel_outbound_end_to_end.rse2e testfile-level #![cfg]

Runtime impact when --no-default-features:

  • Admin RPC /whatsapp/* returns channel-unavailable.
  • HTTP /whatsapp/* route returns 404 (handler block absent).
  • Auto-open Cloudflare quick tunnel for pairing is skipped.
  • Pairing trigger map has no whatsapp entry — admin pairing/start returns "channel not supported".
  • Outbound dispatcher rejects whatsapp routes with typed TranslationError::UnsupportedChannel.

WhatsApp still runs as a discovered subprocess if its manifest sits in plugins.discovery.search_paths and the binary is installed — the gate removes only compile-time imports. Subprocess broker path is unaffected.

Building a telegram-less daemon

cargo build --release --bin nexo --no-default-features

Verify the crate dropped from the dep graph:

cargo tree --no-default-features -i nexo-plugin-telegram
# expected: error: package ID specification `nexo-plugin-telegram` did not match any packages

cargo tree -i nexo-plugin-telegram (without --no-default-features) prints the canonical nexo-rs v0.1.x parent — proving the gate is the only thing keeping telegram in.

Runtime behaviour

A feature-gated build still runs telegram as a discovered subprocess if its manifest sits in plugins.discovery.search_paths and the nexo-plugin-telegram binary is installed (via cargo install nexo-plugin-telegram or release tarball). The gate removes only the daemon's compile-time imports (pairing adapter constructor + outbound-tool fallback registration). The subprocess path uses broker JSON-RPC, not direct Rust imports, so it is unaffected.

Tradeoff: the feature-disabled daemon loses the daemon-side fallback that registers telegram_* outbound tools into the agent's ToolRegistry if the plugin manifest does not yet declare [[plugin.tools.outbound]]. Standalone telegram v0.3.0+ ships the manifest section, so the fallback is dead weight for any operator running a current plugin binary.

CI matrix

The release workflow validates both shapes:

cargo build --bin nexo                        # default (telegram in)
cargo build --bin nexo --no-default-features  # slim (telegram out)

Both targets must compile clean for release-fast and release profiles before the binary ships.

When to add a new feature-gate

Add plugin-<id> = ["dep:nexo-plugin-<id>"] if:

  1. The plugin has a non-trivial Cargo dep with transitive cost (binary size, link time, native dep like OpenSSL).
  2. The plugin is genuinely optional for the target audience (Android, embedded, slim Docker).
  3. The compile-time integration points are localised — no cross-crate admin-RPC entanglement that would force the gate to bubble through crates/setup or crates/core.

If any of (1)-(3) fail, prefer subprocess discovery over a feature-gate — manifest-driven runtime decoupling avoids the conditional-compilation noise.