molecule-core/sdk/python/molecule_plugin/channel.py
Hongming Wang 24fec62d7f initial commit — Molecule AI platform
Forked clean from public hackathon repo (Starfire-AgentTeam, BSL 1.1)
with full rebrand to Molecule AI under github.com/Molecule-AI/molecule-monorepo.

Brand: Starfire → Molecule AI.
Slug: starfire / agent-molecule → molecule.
Env vars: STARFIRE_* → MOLECULE_*.
Go module: github.com/agent-molecule/platform → github.com/Molecule-AI/molecule-monorepo/platform.
Python packages: starfire_plugin → molecule_plugin, starfire_agent → molecule_agent.
DB: agentmolecule → molecule.

History truncated; see public repo for prior commits and contributor
attribution. Verified green: go test -race ./... (platform), pytest
(workspace-template 1129 + sdk 132), vitest (canvas 352), build (mcp).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:55:37 -07:00

113 lines
3.8 KiB
Python

"""Validator for social-channel configurations embedded in org.yaml / direct API payloads.
The platform's Go channel adapters (``platform/internal/channels/``) are the
authoritative implementations (Telegram first, Slack/Discord/WhatsApp on the
roadmap). This module provides a Python-side schema check for the YAML /
JSON blob that users write — so authors catch misspelled fields before the
platform rejects them.
Shape (matches ``platform/internal/handlers/channels.go``):
.. code-block:: yaml
type: telegram
config:
bot_token: ${TELEGRAM_BOT_TOKEN} # platform-resolved env var
chat_id: ${TELEGRAM_CHAT_ID}
enabled: true # default true
Supported types track what the platform knows about via the channel
adapter registry. Keep in sync with ``channels.ChannelAdapter.Type()``.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
import yaml
from .workspace import ValidationError
# Channel types the platform has adapters for, as of today. New adapters
# (slack, discord, whatsapp) are welcome additions — update this set when
# the corresponding Go adapter lands.
SUPPORTED_CHANNEL_TYPES = frozenset({"telegram"})
# Per-type required config keys. Empty tuple = no required keys (for
# adapters that accept zero config).
_REQUIRED_KEYS: dict[str, tuple[str, ...]] = {
"telegram": ("bot_token",),
}
def validate_channel_config(
cfg: dict[str, Any], file_ref: str = "<channel>"
) -> list[ValidationError]:
"""Validate a single channel config dict (not a file)."""
errors: list[ValidationError] = []
ch_type = cfg.get("type")
if not ch_type:
errors.append(ValidationError(file_ref, "missing required field: type"))
return errors
if ch_type not in SUPPORTED_CHANNEL_TYPES:
errors.append(
ValidationError(
file_ref,
f"type={ch_type!r} — must be one of {sorted(SUPPORTED_CHANNEL_TYPES)}",
)
)
return errors
config = cfg.get("config")
if config is not None and not isinstance(config, dict):
errors.append(ValidationError(file_ref, f"config must be an object; got {type(config).__name__}"))
return errors
required = _REQUIRED_KEYS.get(ch_type, ())
for key in required:
if not config or key not in config:
errors.append(
ValidationError(file_ref, f"config.{key} is required for type={ch_type!r}")
)
if "enabled" in cfg and not isinstance(cfg["enabled"], bool):
errors.append(ValidationError(file_ref, f"enabled must be a boolean; got {type(cfg['enabled']).__name__}"))
return errors
def validate_channel_file(path: Path) -> list[ValidationError]:
"""Validate a YAML / JSON file containing a channel config or a list of them."""
if not path.exists():
return [ValidationError(str(path), "file does not exist")]
try:
doc = yaml.safe_load(path.read_text())
except yaml.YAMLError as exc:
return [ValidationError(str(path), f"invalid YAML / JSON: {exc}")]
if doc is None:
return [ValidationError(str(path), "file is empty")]
errors: list[ValidationError] = []
if isinstance(doc, list):
for i, entry in enumerate(doc):
if not isinstance(entry, dict):
errors.append(ValidationError(str(path), f"[{i}]: entry must be an object"))
continue
errors.extend(validate_channel_config(entry, f"{path}[{i}]"))
elif isinstance(doc, dict):
errors.extend(validate_channel_config(doc, str(path)))
else:
errors.append(ValidationError(str(path), f"top-level must be a channel object or list; got {type(doc).__name__}"))
return errors
__all__ = [
"SUPPORTED_CHANNEL_TYPES",
"validate_channel_config",
"validate_channel_file",
]