Notifier interface, Telegram and Signal messaging backends, and secret resolution.
notify defines how leather sends agent output to external messaging systems.
It owns the Notifier interface, the BuildMap factory that constructs
configured backend instances from model.NotifyBackendConfig values, and the
two built-in backends (Telegram Bot API and Signal-CLI HTTP). Secret
resolution (CLI passthrough → environment variable fallback) is encapsulated
here so no other package handles raw credentials.
| Symbol | Signature | Description |
|---|---|---|
Notifier |
interface | Single-method messaging abstraction. |
Notifier.Send |
(ctx context.Context, msg Message) error |
Deliver msg to the backend. Implementations must be safe to call concurrently. |
| Symbol | Description |
|---|---|
Message |
Delivery unit: AgentName string, Content string, Tags []string. |
TelegramNotifier |
Backend targeting the Telegram Bot API. Supports group chats and channels. Retries on HTTP 429. Truncates at 4096 bytes (Telegram's hard limit). |
SignalNotifier |
Backend targeting a signal-cli REST API server. Supports optional auth header and group messaging. |
| Symbol | Signature | Description |
|---|---|---|
BuildMap |
(backends []model.NotifyBackendConfig) (map[string]Notifier, error) |
Construct one Notifier per config entry. Returns error if a backend type is unknown or required fields are missing. |
resolve(ctx context.Context, ref model.SecretRef) (string, error) tries,
in order:
- CLI passthrough — the value is expected to arrive via
ctx.Value(secretCtxKey)map; this is never used in production (reserved for testing). - Environment variable —
os.Getenv(ref.Env)ifref.Envis non-empty. - Returns empty string with no error if both are absent (optional secrets).
Secret values are never logged. Only the env-var name appears in debug output.
- Endpoint:
POST https://api.telegram.org/bot<token>/sendMessage - Format:
*[AgentName]* content(MarkdownV2) - Tags rendered as
#tagprefix if non-empty - 429 retry: back off 5 s, retry once
- Content truncated to 4096 UTF-8 bytes (last code-point boundary) before send
- Token resolved via
model.SecretRef→resolve()
- Endpoint:
POST <baseURL>/v2/send - Optional
Authorizationheader frommodel.SecretRef - Supports
recipient(phone number) orgroup_id(base64 group ID) - Body serialized as JSON:
{message, recipients: [...]}or{message, groupId} - Non-2xx responses returned as errors
switch cfg.Type {
case "telegram": // build TelegramNotifier
case "signal": // build SignalNotifier
default: // return error (fail closed)
}Unknown types fail closed rather than being silently skipped, matching the leather "fail closed" design rule.
| Package | Why |
|---|---|
internal/model |
NotifyBackendConfig, SecretRef types |
internal/notify has no other intra-project imports. It is a leaf package.
flowchart LR
CFG[NotifyBackendConfig] --> BM[BuildMap]
BM --> NM[map: name→Notifier]
Runner -->|route.NotifyBackend| NM
NM --> SND[Notifier.Send]
SND -->|HTTP POST| TG[Telegram API]
SND -->|HTTP POST| SG[Signal-CLI API]
internal/notify/notify_test.go — 20 tests using httptest.NewServer:
BuildMap: unknown type rejected, empty config returns empty mapTelegramNotifier.Send: success path, 429 retry, content truncationSignalNotifier.Send: recipient path, group_id path, auth header, non-2xx errorformatTelegram: tag rendering, no-tag renderingformatSignal: message formatting- Secret resolution: env var hit, env var missing → empty string, unknown ctx key
- docs/modules/runner.md — issues
Notifier.Sendvia output routing - docs/ARCHITECTURE.md — notify in the output routing table