channels

package
v0.19.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package channels defines the channel adapter architecture for exposing self-hosted agents via messaging platforms like Slack and Telegram.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ResolveEnvVars

func ResolveEnvVars(cfg *ChannelConfig) map[string]string

ResolveEnvVars inspects cfg.Settings for keys ending in "_env" and resolves them from the environment. For example, a setting "bot_token_env": "SLACK_BOT_TOKEN" produces {"bot_token": os.Getenv("SLACK_BOT_TOKEN")}. Non-env settings are passed through unchanged.

func StartDeliverSpan added in v0.16.0

func StartDeliverSpan(ctx context.Context, adapter string, event *ChannelEvent) (context.Context, trace.Span, func(*error))

StartDeliverSpan opens a `channel.<adapter>.deliver` span around an inbound channel adapter's per-message handler and returns a finish closure that records the error and ends the span. The span attributes mirror the upstream-system metadata an operator needs to pivot from the trace back to the Slack thread / Telegram chat / Teams message that produced this invocation. Issue #187.

The span PARENTS the internal A2A POST that `forge-cli/channels/router.go` issues, because that router injects the W3C traceparent from the calling ctx onto the outbound HTTP request — the A2A server's `a2a.tasks/send` span then nests under this delivery span instead of starting as an orphan root.

Usage shape (defer captures the named return so finish sees the final err value):

go func() {
    ctx, _, finish := channels.StartDeliverSpan(ctx, "slack", event)
    var err error
    defer finish(&err)
    // ... handler work mutates err ...
}()

When tracing is disabled the global tracer is a no-op and Start returns an empty span with zero allocations — safe to call on every inbound message regardless of tracing posture.

Types

type ApprovalDecision added in v0.18.1

type ApprovalDecision struct {
	TaskID   string // must match the ApprovalRequest.TaskID
	Decision string // "approve" | "reject"
	Approver string // who acted (platform user id / name), for audit
	// ApproverEmail is the approver's resolved email (#313). Adapters populate
	// it (e.g. Slack via users.info) so the runtime's approver allowlist check
	// is adapter-agnostic. Empty when the adapter couldn't resolve it — the
	// runtime fails closed against a configured allowlist.
	ApproverEmail string
	Note          string // optional justification
}

ApprovalDecision is an approver's response, delivered back to the runtime by the adapter that received the interaction.

type ApprovalDeliverer added in v0.18.1

type ApprovalDeliverer interface {
	// DeliverApproval posts the interactive approval request to req.Target.
	DeliverApproval(ctx context.Context, req ApprovalRequest) error
	// SetApprovalResolver wires the callback the adapter invokes when an
	// approver acts. The runtime sets this once at startup.
	SetApprovalResolver(r ApprovalResolver)
}

ApprovalDeliverer is an OPTIONAL capability. A channel adapter that can post an interactive approval request AND receive the approver's response implements it (Slack via Block Kit over Socket Mode, #310). Adapters that don't implement it simply can't be a DEFER `to:` target; the deferral still works via a direct POST /tasks/{id}/decisions.

type ApprovalRequest added in v0.18.1

type ApprovalRequest struct {
	TaskID  string        // the deferred A2A task; the resolution key
	Tool    string        // the tool call awaiting approval
	Context string        // rendered context_template (what the agent wants to do)
	Timeout time.Duration // how long until auto-deny
	Target  string        // adapter-specific destination (e.g. a Slack channel "#oncall")
}

ApprovalRequest is a pending human-approval to deliver to an approver via an interactive channel message (e.g. Slack Block Kit buttons). The runtime builds one when a tool call is deferred; a channel adapter renders it.

type ApprovalResolver added in v0.18.1

type ApprovalResolver func(ctx context.Context, d ApprovalDecision) error

ApprovalResolver is invoked by an adapter when an approver acts on a delivered ApprovalRequest. The runtime routes it to the deferred task's decision (typically POST /tasks/{id}/decisions). Wired via ApprovalDeliverer.SetApprovalResolver at startup.

type Attachment

type Attachment struct {
	Name     string `json:"name,omitempty"`
	MimeType string `json:"mime_type,omitempty"`
	URL      string `json:"url,omitempty"`
}

Attachment represents a file or media item attached to a channel message.

type ChannelConfig

type ChannelConfig struct {
	Adapter     string            `yaml:"adapter"`
	WebhookPort int               `yaml:"webhook_port,omitempty"`
	WebhookPath string            `yaml:"webhook_path,omitempty"`
	Settings    map[string]string `yaml:"settings,omitempty"`
}

ChannelConfig holds per-adapter configuration loaded from YAML.

type ChannelEvent

type ChannelEvent struct {
	Channel     string `json:"channel"`
	WorkspaceID string `json:"workspace_id"`
	UserID      string `json:"user_id"`
	// UserEmail is the sender's resolved email (e.g. Slack users.info,
	// cached), when the adapter can determine it. It rides to the A2A server
	// as the task's on-behalf-of identity so delegated (auth.type=user) MCP
	// tools key consent + per-user tokens on the HUMAN who sent the message,
	// not the runtime's loopback identity (field-hit 2026-07-21: a consent DM
	// addressed to "forge-internal"). Best-effort — empty when unresolvable.
	UserEmail   string          `json:"user_email,omitempty"`
	ThreadID    string          `json:"thread_id,omitempty"`
	MessageID   string          `json:"message_id,omitempty"` // per-message ID for reply targeting
	Message     string          `json:"message"`
	Attachments []Attachment    `json:"attachments,omitempty"`
	Raw         json.RawMessage `json:"raw,omitempty"`
}

ChannelEvent is the normalized representation of an inbound message from any supported platform.

type ChannelOrigin added in v0.18.1

type ChannelOrigin struct {
	Adapter  string // e.g. "slack"
	Channel  string // native channel / DM id
	ThreadTS string // thread to reply in (optional)
	UserID   string // native user id (skips an email lookup)
}

ChannelOrigin locates where a request came from so consent can be presented where the user is already talking (an in-thread reply) rather than a cold DM. An adapter populates it on inbound; nil ⇒ the deliverer falls back to reaching the Subject directly (e.g. Slack DM by email).

type ChannelPlugin

type ChannelPlugin interface {
	// Name returns the adapter name (e.g. "slack", "telegram").
	Name() string
	// Init configures the plugin from a ChannelConfig.
	Init(cfg ChannelConfig) error
	// Start begins listening for events and dispatching them to handler.
	// It blocks until ctx is cancelled.
	Start(ctx context.Context, handler EventHandler) error
	// Stop gracefully shuts down the plugin.
	Stop() error
	// NormalizeEvent converts raw platform bytes into a ChannelEvent.
	NormalizeEvent(raw []byte) (*ChannelEvent, error)
	// SendResponse delivers an A2A response back to the originating platform.
	SendResponse(event *ChannelEvent, response *a2a.Message) error
}

ChannelPlugin is the interface every channel adapter must implement.

type ConsentCanceler added in v0.18.1

type ConsentCanceler func(ctx context.Context, subject, server string) error

ConsentCanceler is invoked by an adapter when the user cancels a delivered consent prompt (e.g. a "Cancel" button). The runtime fails the parked call fast instead of idling to the deadline. Wired via ConsentDeliverer.SetConsentCanceler at startup; optional.

type ConsentDeliverer added in v0.18.1

type ConsentDeliverer interface {
	// DeliverConsent presents the consent prompt to req.Subject (or req.Origin).
	DeliverConsent(ctx context.Context, req ConsentPrompt) error
	// SetConsentCanceler wires the callback the adapter invokes when the user
	// cancels. The runtime sets this once at startup; may be a no-op.
	SetConsentCanceler(c ConsentCanceler)
}

ConsentDeliverer is an OPTIONAL capability. A channel adapter that can present an MCP consent login link to a specific user implements it (Slack via a DM / in-thread Block Kit message, #343). Adapters that don't implement it simply can't deliver consent prompts; the runtime falls back to publishing the link on the A2A auth-required artifact.

type ConsentPrompt added in v0.18.1

type ConsentPrompt struct {
	Subject      string         // the requesting user (email preferred) — who to reach
	Server       string         // MCP server name — for the prompt copy
	AuthorizeURL string         // the login link the user opens (a URL button)
	Deadline     time.Time      // the gate timeout — rendered as "expires …"
	Origin       *ChannelOrigin // optional: reply in the origin thread if the request came via this channel
}

ConsentPrompt is a pending MCP delegated-consent prompt: a "Connect <server>" login link to present to the requesting user so they can authorize the agent to act as them. The runtime builds one when a type: user MCP call parks on the auth-required gate (#330); a channel adapter presents it.

Delivery is independent of who built the URL and who hosts the callback: AuthorizeURL is opaque to the adapter (standalone → Forge-built; managed → platform-supplied), so the same delivery code serves both modes.

type EventHandler

type EventHandler func(ctx context.Context, event *ChannelEvent) (*a2a.Message, error)

EventHandler is the callback signature provided by the router. The plugin calls it when a message arrives; the handler forwards the event to the A2A server and returns the agent's response.

type Logger added in v0.18.1

type Logger interface {
	Info(msg string, fields map[string]any)
	Warn(msg string, fields map[string]any)
	Error(msg string, fields map[string]any)
	Debug(msg string, fields map[string]any)
}

Logger is the minimal structured ops logger a channel adapter uses for operational signals (the FWS-9 stdout ops stream). Satisfied by forge-core/runtime.Logger, so the runtime can wire its logger without this package importing it.

type LoggerAware added in v0.18.1

type LoggerAware interface {
	SetLogger(Logger)
}

LoggerAware is an OPTIONAL capability: an adapter that routes operational signals through a structured logger implements it. The runtime wires it at startup; adapters that don't implement it keep their own logging.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry holds registered channel plugins keyed by name.

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates an empty plugin registry.

func (*Registry) Get

func (r *Registry) Get(name string) ChannelPlugin

Get returns the plugin with the given name, or nil if not found.

func (*Registry) Register

func (r *Registry) Register(p ChannelPlugin)

Register adds a plugin to the registry, keyed by its Name().

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL