Skip to content

Commit 16653e1

Browse files
authored
refactor(integrations): registry one for-loop, schema-driven (#6386)
Eliminate the hand-maintained integrations registry by deriving the catalog from schema sources. Closes #6294. - `Configurable` derive emits `nested_option_entries(&self)` for `#[nested] Option<T>` fields. The registry consumes this on `ChannelsConfig`, so each new `pub foo: Option<FooConfig>` channel surfaces an integration entry automatically. Surfaces ~15 channels the prior hand-list missed (Mattermost, IRC, Lark, Line, Feishu, WeCom, WeChat, Reddit, Bluesky, MQTT, Discord History, Voice Call/Wake/Duplex, ClawdTalk, Gmail Push). - AI providers derive from `ProviderInfo` + a new `ProviderActivation` enum (`AlwaysActive`, `EnvVarPresent`, `ConfigKeyPresent`, `ConfigPredicate`, `FallbackKeyMatches`). Zero per-vendor branches; registry.rs shrinks 809 -> 142 lines. - Per-field `#[display_name]`/`#[description]` attributes and a struct-level `#[integration(...)]` attribute push all metadata to the schema side; production registry path has zero string literals naming a channel/vendor/tool/platform. - `ComingSoon` removed entirely (enum variant, hardcoded entries, frontend type union, statusBadge case, 62 i18n strings). Beta-tier breaking changes (recorded in CHANGELOG-next.md): - `IntegrationStatus::ComingSoon` removed. - `IntegrationCategory` variants removed: `Productivity`, `MusicAudio`, `SmartHome`, `MediaCreative`, `Social` (no live entries). `Google Workspace` recategorised to `ToolsAutomation`. - `IntegrationEntry.status_fn` -> `IntegrationEntry.status` (eager). - `all_integrations()` now takes `&Config`. Operator surface: dashboard renders more channels, no "Coming Soon" entries. Config files unchanged. Closes #6294 Co-authored-by: Shane Engelman <contact@shane.gg>
1 parent 9544b13 commit 16653e1

13 files changed

Lines changed: 848 additions & 1145 deletions

File tree

CHANGELOG-next.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@
4646

4747
---
4848

49+
## Breaking changes
50+
51+
### zeroclaw-runtime (Beta)
52+
53+
- `IntegrationStatus::ComingSoon` removed. Callers that match on it must drop that arm. Hand-written "planned" entries are gone; if a channel or tool is not in the schema or not a real runtime built-in, it does not appear in the integrations registry.
54+
- `IntegrationCategory` variants `Productivity`, `MusicAudio`, `SmartHome`, `MediaCreative`, `Social` removed. Downstream `match` exhaustiveness will break at compile time. These categories had no live entries (only the now-removed `ComingSoon` placeholders).
55+
- `Google Workspace` recategorised from `Productivity` (removed) to `ToolsAutomation`.
56+
- `IntegrationEntry.status_fn: fn(&Config) -> IntegrationStatus` replaced by `IntegrationEntry.status: IntegrationStatus`. The catalog is now evaluated eagerly inside `all_integrations(&Config)` rather than carrying a per-entry closure.
57+
- `all_integrations()` signature changed from `() -> Vec<IntegrationEntry>` to `(&Config) -> Vec<IntegrationEntry>`. Callers must thread a `Config` reference.
58+
59+
---
60+
4961
## What's New
5062

5163
### Architecture & Workspace

crates/zeroclaw-config/src/schema.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2772,6 +2772,12 @@ impl Default for BrowserComputerUseConfig {
27722772
#[derive(Debug, Clone, Serialize, Deserialize, Configurable)]
27732773
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
27742774
#[prefix = "browser"]
2775+
#[integration(
2776+
category = "ToolsAutomation",
2777+
display_name = "Browser",
2778+
description = "Chrome/Chromium control",
2779+
status_field = "enabled"
2780+
)]
27752781
pub struct BrowserConfig {
27762782
/// Enable `browser_open` tool (opens URLs in the system browser without scraping)
27772783
#[serde(default = "default_true")]
@@ -3435,6 +3441,12 @@ pub struct GoogleWorkspaceAllowedOperation {
34353441
#[derive(Debug, Clone, Serialize, Deserialize, Configurable)]
34363442
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
34373443
#[prefix = "google-workspace"]
3444+
#[integration(
3445+
category = "ToolsAutomation",
3446+
display_name = "Google Workspace",
3447+
description = "Drive, Gmail, Calendar, Sheets, Docs via gws CLI",
3448+
status_field = "enabled"
3449+
)]
34383450
pub struct GoogleWorkspaceConfig {
34393451
/// Enable the `google_workspace` tool. Default: `false`.
34403452
#[serde(default)]
@@ -6375,6 +6387,12 @@ impl Default for HeartbeatConfig {
63756387
#[derive(Debug, Clone, Serialize, Deserialize, Configurable)]
63766388
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
63776389
#[prefix = "cron"]
6390+
#[integration(
6391+
category = "ToolsAutomation",
6392+
display_name = "Cron",
6393+
description = "Scheduled tasks",
6394+
status_field = "enabled"
6395+
)]
63786396
pub struct CronConfig {
63796397
/// Enable the cron subsystem. Default: `true`.
63806398
#[serde(default = "default_true")]
@@ -6702,103 +6720,169 @@ pub struct ChannelsConfig {
67026720
pub cli: bool,
67036721
/// Telegram bot channel configuration.
67046722
#[nested]
6723+
#[display_name = "Telegram"]
6724+
#[description = "Bot API — long-polling"]
67056725
pub telegram: Option<TelegramConfig>,
67066726
/// Discord bot channel configuration.
67076727
#[nested]
6728+
#[display_name = "Discord"]
6729+
#[description = "Servers, channels & DMs"]
67086730
pub discord: Option<DiscordConfig>,
67096731
/// Discord history channel — logs ALL messages and forwards @mentions to agent.
67106732
#[nested]
6733+
#[display_name = "Discord History"]
6734+
#[description = "Logs all messages, forwards mentions to the agent"]
67116735
pub discord_history: Option<DiscordHistoryConfig>,
67126736
/// Slack bot channel configuration.
67136737
#[nested]
6738+
#[display_name = "Slack"]
6739+
#[description = "Workspace apps via Web API"]
67146740
pub slack: Option<SlackConfig>,
67156741
/// Mattermost bot channel configuration.
67166742
#[nested]
6743+
#[display_name = "Mattermost"]
6744+
#[description = "Self-hosted team chat"]
67176745
pub mattermost: Option<MattermostConfig>,
67186746
/// Webhook channel configuration.
67196747
#[nested]
6748+
#[display_name = "Webhooks"]
6749+
#[description = "HTTP endpoint for triggers"]
67206750
pub webhook: Option<WebhookConfig>,
67216751
/// iMessage channel configuration (macOS only).
67226752
#[nested]
6753+
#[display_name = "iMessage"]
6754+
#[description = "macOS AppleScript bridge"]
67236755
pub imessage: Option<IMessageConfig>,
67246756
/// Matrix channel configuration.
67256757
#[nested]
6758+
#[display_name = "Matrix"]
6759+
#[description = "Matrix protocol (Element)"]
67266760
pub matrix: Option<MatrixConfig>,
67276761
/// Signal channel configuration.
67286762
#[nested]
6763+
#[display_name = "Signal"]
6764+
#[description = "Privacy-focused via signal-cli"]
67296765
pub signal: Option<SignalConfig>,
67306766
/// WhatsApp channel configuration (Cloud API or Web mode).
67316767
#[nested]
6768+
#[display_name = "WhatsApp"]
6769+
#[description = "Meta Cloud API or Web mode"]
67326770
pub whatsapp: Option<WhatsAppConfig>,
67336771
/// Linq Partner API channel configuration.
67346772
#[nested]
6773+
#[display_name = "Linq"]
6774+
#[description = "Linq Partner API for iMessage/RCS/SMS"]
67356775
pub linq: Option<LinqConfig>,
67366776
/// WATI WhatsApp Business API channel configuration.
67376777
#[nested]
6778+
#[display_name = "WATI"]
6779+
#[description = "WhatsApp Business API gateway"]
67386780
pub wati: Option<WatiConfig>,
67396781
/// Nextcloud Talk bot channel configuration.
67406782
#[nested]
6783+
#[display_name = "Nextcloud Talk"]
6784+
#[description = "Self-hosted Nextcloud chat"]
67416785
pub nextcloud_talk: Option<NextcloudTalkConfig>,
67426786
/// Email channel configuration.
67436787
#[nested]
6788+
#[display_name = "Email"]
6789+
#[description = "IMAP / SMTP inbox bridge"]
67446790
pub email: Option<crate::scattered_types::EmailConfig>,
67456791
/// Gmail Pub/Sub push notification channel configuration.
67466792
#[nested]
6793+
#[display_name = "Gmail Push"]
6794+
#[description = "Pub/Sub push notifications for Gmail"]
67476795
pub gmail_push: Option<crate::scattered_types::GmailPushConfig>,
67486796
/// IRC channel configuration.
67496797
#[nested]
6798+
#[display_name = "IRC"]
6799+
#[description = "Classic IRC with SASL / NickServ"]
67506800
pub irc: Option<IrcConfig>,
67516801
/// Lark channel configuration.
67526802
#[nested]
6803+
#[display_name = "Lark"]
6804+
#[description = "ByteDance Lark / Feishu international"]
67536805
pub lark: Option<LarkConfig>,
67546806
/// LINE Messaging API channel configuration.
67556807
#[nested]
6808+
#[display_name = "LINE"]
6809+
#[description = "LINE Messaging API"]
67566810
pub line: Option<LineConfig>,
67576811
/// Feishu channel configuration.
67586812
#[nested]
6813+
#[display_name = "Feishu"]
6814+
#[description = "ByteDance Feishu (China)"]
67596815
pub feishu: Option<FeishuConfig>,
67606816
/// DingTalk channel configuration.
67616817
#[nested]
6818+
#[display_name = "DingTalk"]
6819+
#[description = "DingTalk Stream Mode"]
67626820
pub dingtalk: Option<DingTalkConfig>,
67636821
/// WeCom (WeChat Enterprise) Bot Webhook channel configuration.
67646822
#[nested]
6823+
#[display_name = "WeCom"]
6824+
#[description = "WeChat Enterprise Bot Webhook"]
67656825
pub wecom: Option<WeComConfig>,
67666826
/// WeChat personal iLink Bot channel configuration (QR code login).
67676827
#[nested]
6828+
#[display_name = "WeChat"]
6829+
#[description = "WeChat personal iLink Bot (QR login)"]
67686830
pub wechat: Option<WeChatConfig>,
67696831
/// QQ Official Bot channel configuration.
67706832
#[nested]
6833+
#[display_name = "QQ Official"]
6834+
#[description = "Tencent QQ Bot SDK"]
67716835
pub qq: Option<QQConfig>,
67726836
/// X/Twitter channel configuration.
67736837
#[nested]
6838+
#[display_name = "X / Twitter"]
6839+
#[description = "X / Twitter API"]
67746840
pub twitter: Option<TwitterConfig>,
67756841
/// Mochat customer service channel configuration.
67766842
#[nested]
6843+
#[display_name = "Mochat"]
6844+
#[description = "Mochat customer service"]
67776845
pub mochat: Option<MochatConfig>,
67786846
#[cfg(feature = "channel-nostr")]
67796847
#[nested]
6848+
#[display_name = "Nostr"]
6849+
#[description = "Decentralized DMs (NIP-04)"]
67806850
pub nostr: Option<NostrConfig>,
67816851
/// ClawdTalk voice channel configuration.
67826852
#[nested]
6853+
#[display_name = "ClawdTalk"]
6854+
#[description = "ClawdTalk voice channel"]
67836855
pub clawdtalk: Option<crate::scattered_types::ClawdTalkConfig>,
67846856
/// Reddit channel configuration (OAuth2 bot).
67856857
#[nested]
6858+
#[display_name = "Reddit"]
6859+
#[description = "Reddit OAuth2 bot"]
67866860
pub reddit: Option<RedditConfig>,
67876861
/// Bluesky channel configuration (AT Protocol).
67886862
#[nested]
6863+
#[display_name = "Bluesky"]
6864+
#[description = "Bluesky / AT Protocol"]
67896865
pub bluesky: Option<BlueskyConfig>,
67906866
/// Voice call channel configuration (Twilio/Telnyx/Plivo).
67916867
#[nested]
6868+
#[display_name = "Voice Call"]
6869+
#[description = "Twilio / Telnyx / Plivo voice calls"]
67926870
pub voice_call: Option<crate::scattered_types::VoiceCallConfig>,
67936871
/// Voice wake word detection channel configuration.
67946872
#[cfg(feature = "voice-wake")]
67956873
#[nested]
6874+
#[display_name = "Voice Wake"]
6875+
#[description = "Local wake-word detection"]
67966876
pub voice_wake: Option<VoiceWakeConfig>,
67976877
/// Voice duplex configuration (full-duplex voice over WebSocket).
67986878
#[nested]
6879+
#[display_name = "Voice Duplex"]
6880+
#[description = "Full-duplex voice over WebSocket"]
67996881
pub voice_duplex: Option<VoiceDuplexConfig>,
68006882
/// MQTT channel configuration (SOP listener).
68016883
#[nested]
6884+
#[display_name = "MQTT"]
6885+
#[description = "MQTT SOP listener"]
68026886
pub mqtt: Option<MqttConfig>,
68036887
/// Base timeout in seconds for processing a single channel message (LLM + tools).
68046888
/// Runtime uses this as a per-turn budget that scales with tool-loop depth
@@ -10017,6 +10101,19 @@ async fn ensure_bootstrap_files(workspace_dir: &Path) -> Result<()> {
1001710101
}
1001810102

1001910103
impl Config {
10104+
/// Collect the `IntegrationDescriptor` from every nested config that
10105+
/// declares one via `#[integration(...)]`. Adding a new toggleable
10106+
/// integration is one struct-level attribute on the new config + one
10107+
/// row in this method. The integrations registry consumes the result
10108+
/// without per-vendor branches.
10109+
pub fn integration_descriptors(&self) -> Vec<crate::config::IntegrationDescriptor> {
10110+
vec![
10111+
self.browser.integration_descriptor(),
10112+
self.cron.integration_descriptor(),
10113+
self.google_workspace.integration_descriptor(),
10114+
]
10115+
}
10116+
1002010117
/// Combine top-level `[cost.prices.<key>]` entries with any per-provider
1002110118
/// `pricing` entries declared on `[providers.models.<id>]`. Per-provider
1002210119
/// pricing is keyed as `<provider_id>/<model>` to align with the lookup

crates/zeroclaw-config/src/traits.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,45 @@ pub struct MapKeySection {
149149
pub description: &'static str,
150150
}
151151

152+
/// One row emitted by the `Configurable` derive's `nested_option_entries()`
153+
/// method — every `#[nested] Option<XConfig>` field on a struct shows up here
154+
/// with its `present` bit and the per-field `#[display_name = "..."]` /
155+
/// `#[description = "..."]` metadata. The integrations registry consumes
156+
/// this verbatim instead of carrying its own per-field hand-list.
157+
#[derive(Debug, Clone, Copy)]
158+
pub struct NestedOptionEntry {
159+
/// snake_case field name on the parent struct (e.g. `"telegram"`,
160+
/// `"voice_duplex"`).
161+
pub field: &'static str,
162+
/// `true` when the parent struct's field is `Some(_)`.
163+
pub present: bool,
164+
/// Display name from `#[display_name = "..."]`; falls back to a
165+
/// title-cased rendering of the snake_case field name when the
166+
/// attribute is absent.
167+
pub display_name: &'static str,
168+
/// One-line summary from `#[description = "..."]`. Empty when the
169+
/// attribute is absent.
170+
pub description: &'static str,
171+
}
172+
173+
/// One row emitted by the `Configurable` derive's `integration_descriptor()`
174+
/// method on structs annotated with `#[integration(...)]`. Used for nested
175+
/// toggleable configs (e.g. `BrowserConfig`, `CronConfig`) where the
176+
/// integration is "active" iff a named bool field on the struct is `true`.
177+
#[derive(Debug, Clone, Copy)]
178+
pub struct IntegrationDescriptor {
179+
pub display_name: &'static str,
180+
pub description: &'static str,
181+
/// Free-form category label (e.g. `"ToolsAutomation"`). The
182+
/// integrations registry maps this string to its own
183+
/// `IntegrationCategory` enum so the schema crate doesn't have to
184+
/// depend on it.
185+
pub category: &'static str,
186+
/// Snapshot of the named status field at the moment this descriptor
187+
/// was built (`status_field = "enabled"` ⇒ `self.enabled`).
188+
pub active: bool,
189+
}
190+
152191
/// The trait for describing a channel
153192
pub trait ChannelConfig {
154193
/// human-readable name

crates/zeroclaw-gateway/src/api.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -574,17 +574,16 @@ pub async fn handle_api_integrations(
574574
}
575575

576576
let config = state.config.lock().clone();
577-
let entries = zeroclaw_runtime::integrations::registry::all_integrations();
577+
let entries = zeroclaw_runtime::integrations::registry::all_integrations(&config);
578578

579579
let integrations: Vec<serde_json::Value> = entries
580580
.iter()
581581
.map(|entry| {
582-
let status = (entry.status_fn)(&config);
583582
serde_json::json!({
584583
"name": entry.name,
585584
"description": entry.description,
586585
"category": entry.category,
587-
"status": status,
586+
"status": entry.status,
588587
})
589588
})
590589
.collect();
@@ -602,21 +601,20 @@ pub async fn handle_api_integrations_settings(
602601
}
603602

604603
let config = state.config.lock().clone();
605-
let entries = zeroclaw_runtime::integrations::registry::all_integrations();
604+
let entries = zeroclaw_runtime::integrations::registry::all_integrations(&config);
606605

607606
let mut settings = serde_json::Map::new();
608607
for entry in &entries {
609-
let status = (entry.status_fn)(&config);
610608
let enabled = matches!(
611-
status,
609+
entry.status,
612610
zeroclaw_runtime::integrations::IntegrationStatus::Active
613611
);
614612
settings.insert(
615-
entry.name.to_string(),
613+
entry.name.clone(),
616614
serde_json::json!({
617615
"enabled": enabled,
618616
"category": entry.category,
619-
"status": status,
617+
"status": entry.status,
620618
}),
621619
);
622620
}

0 commit comments

Comments
 (0)