mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
A connector chat whose sender holds talkback.channels gets the tool send_message. It writes in the conversation, or as a new top-level message in a channel that the owner listed on the binding (id, label, approved; at most 10). The tool names no platform. An unlisted channel is refused and the error lists the ones that are. A listed channel that the owner did not approve needs the sensitive talkback.new_channel for each message, so only a paired sender can use it. The agent cannot add or approve a channel, so text it read in a page or a file cannot widen where it writes. Messages go through the outbox (rate limit and queue), with the size cap and the mention breaking. Each attempt adds an audit row with channel, kind, capability, length and a hash, never the text. A trigger can report to an approved channel with delivery target channel. The binding, the capability and the listing are checked again when the run ends; any failure sends the notice to the Inbox. LOCALAI_DISABLE_CONNECTOR_TALKBACK turns both off. Assisted-by: Claude:claude-sonnet-5-5 go swag
81 lines
2.9 KiB
Go
81 lines
2.9 KiB
Go
package studio
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/mudler/LocalAI/core/services/studio/builtins"
|
|
"github.com/mudler/LocalAI/core/services/studio/proposals"
|
|
"github.com/mudler/LocalAI/core/services/studio/triggers"
|
|
"github.com/mudler/LocalAI/pkg/addons/contract"
|
|
)
|
|
|
|
// checkChannelTarget is what a proposal that reports to a channel must pass at
|
|
// once, so the model hears the reason while the person is there: the sender
|
|
// holds talkback.channels, and the channel is one the owner listed and
|
|
// approved. A run has no sender at delivery time, so a channel that waits for
|
|
// a paired sender is not a target for reports.
|
|
func (h *Triggers) checkChannelTarget(ctx context.Context, c builtins.Chat, channel string) error {
|
|
conns := h.svc.connectors
|
|
if conns == nil {
|
|
return errors.New("connections to messaging apps are off on this server")
|
|
}
|
|
b, err := conns.o.Store.GetBinding(ctx, c.OriginID)
|
|
if err != nil {
|
|
return errors.New("this connection is gone")
|
|
}
|
|
kind, _, terr := ChannelTarget(b, channel)
|
|
if terr != nil {
|
|
return terr
|
|
}
|
|
if kind != SendChannel {
|
|
return fmt.Errorf("the channel %q is not approved for reports; the owner approves it in Studio", channel)
|
|
}
|
|
return c.Require(contract.CapTalkbackChannels)
|
|
}
|
|
|
|
// deliverChannel writes body in the channel of the trigger. It checks, now and
|
|
// not when the trigger was made, that the binding is on, the capability is held
|
|
// by the binding, and the channel is still listed and approved. Any failure
|
|
// returns false so that the Inbox gets the notice. fallback runs when a queued
|
|
// message cannot be sent later.
|
|
func (h *Triggers) deliverChannel(ctx context.Context, t triggers.Trigger, body string, fallback func()) bool {
|
|
c := h.svc.connectors
|
|
refuse := func(why string) bool {
|
|
h.audit(ctx, proposals.ActionDeliver, t, "refused: "+why)
|
|
return false
|
|
}
|
|
if c == nil {
|
|
return refuse("connectors are off")
|
|
}
|
|
b, err := c.o.Store.GetBinding(ctx, t.Route.Binding)
|
|
if err != nil || !b.Enabled {
|
|
return refuse("the binding is off")
|
|
}
|
|
kind, _, terr := ChannelTarget(b, t.Route.Channel)
|
|
switch {
|
|
case terr != nil:
|
|
return refuse("the channel is not listed")
|
|
case kind != SendChannel:
|
|
return refuse("the channel is not approved")
|
|
case !h.svc.allowCap(ctx, CapabilityRequest{Capability: contract.CapTalkbackChannels, Binding: t.Route.Binding, Owner: t.Owner, Agent: t.AgentID}):
|
|
return refuse("capability off")
|
|
}
|
|
queued, err := c.Send(ctx, Outgoing{Binding: b.ID, Chat: t.Route.Channel, Text: body, Kind: SendDelivery, Capability: contract.CapTalkbackChannels,
|
|
OnFail: func(error) {
|
|
h.audit(context.Background(), proposals.ActionDeliver, t, "failed; sent to the Inbox")
|
|
fallback()
|
|
}})
|
|
if err != nil {
|
|
h.audit(ctx, proposals.ActionDeliver, t, "failed; sent to the Inbox: "+err.Error())
|
|
return false
|
|
}
|
|
if queued {
|
|
h.audit(ctx, proposals.ActionDeliver, t, "channel, queued")
|
|
} else {
|
|
h.audit(ctx, proposals.ActionDeliver, t, "channel, sent")
|
|
}
|
|
return true
|
|
}
|