Never send a nudge body off-box when the summary is empty (#368)
Away channels (ntfy, telegram) leave the box, so an empty Summary now sends a fixed generic line plus the rule name instead of the full Body. The dispatcher strips detail before any sink sees it, so a sink added later cannot leak by reading the wrong field. Voice is local and unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
This commit is contained in:
@@ -160,6 +160,7 @@ func (d *Dispatcher) DispatchNudge(ctx context.Context, pn PhrasedNudge, now tim
|
||||
RepeatUntilAck: ch == ChannelTelegram && c.Severity >= loop.Sev4,
|
||||
Ts: now,
|
||||
}
|
||||
s = minimalForAway(s)
|
||||
sink := d.sinkFor(ch)
|
||||
if sink == nil {
|
||||
continue
|
||||
@@ -225,6 +226,7 @@ func (d *Dispatcher) DispatchReminder(ctx context.Context, pr PhrasedReminder, n
|
||||
Summary: pr.Summary,
|
||||
Ts: now,
|
||||
}
|
||||
s = minimalForAway(s)
|
||||
sink := d.sinkFor(ch)
|
||||
if sink == nil {
|
||||
continue
|
||||
@@ -315,6 +317,7 @@ func (d *Dispatcher) RepeatUnacked(ctx context.Context, keys []string, now time.
|
||||
RepeatUntilAck: true,
|
||||
Ts: now,
|
||||
}
|
||||
s = minimalForAway(s)
|
||||
attemptID := d.beginOutbox(ctx, "nudge", key, 0, ChannelTelegram, messageForChannel(s), now)
|
||||
if err := d.cfg.Telegram.Send(ctx, s); err != nil {
|
||||
d.completeOutbox(ctx, attemptID, store.DeliveryFailed, now)
|
||||
@@ -342,20 +345,44 @@ func (d *Dispatcher) sinkFor(ch Channel) Sink {
|
||||
}
|
||||
}
|
||||
|
||||
// GenericAwayMessage — what an away channel gets when the phraser gave us no
|
||||
// summary. no gendered forms, so it stays right whoever reads it.
|
||||
const GenericAwayMessage = "что-то требует внимания"
|
||||
|
||||
// isAway — this channel leaves the box, so it only ever gets a minimal body.
|
||||
func isAway(ch Channel) bool {
|
||||
return ch == ChannelNtfy || ch == ChannelTelegram
|
||||
}
|
||||
|
||||
// messageForChannel — away channels get the minimal summary (no shoulder-surf
|
||||
// exfil — "disk low on homesrv," not detail); voice gets the full body (local).
|
||||
// a missing summary falls back to body — a terse full message is better than
|
||||
// no message, and the phraser should have produced a summary for away-bound
|
||||
// severities. this is the "minimal body" rule from the spec, enforced at the
|
||||
// last mile so a phraser bug can't accidentally exfil via the relay.
|
||||
// an empty summary must NOT fall back to the body: the resident model is small
|
||||
// and drops fields often, and the away path crosses the "never phones home"
|
||||
// boundary. so we send a fixed generic line plus the rule name instead. voice
|
||||
// is local, so it keeps the full body.
|
||||
func messageForChannel(s Sendable) string {
|
||||
switch s.Channel {
|
||||
case ChannelNtfy, ChannelTelegram:
|
||||
if s.Summary != "" {
|
||||
return s.Summary
|
||||
}
|
||||
return s.Body
|
||||
default:
|
||||
if !isAway(s.Channel) {
|
||||
return s.Body
|
||||
}
|
||||
if s.Summary != "" {
|
||||
return s.Summary
|
||||
}
|
||||
if s.RuleName != "" {
|
||||
return GenericAwayMessage + ": " + s.RuleName
|
||||
}
|
||||
return GenericAwayMessage
|
||||
}
|
||||
|
||||
// minimalForAway — strips detail from a Sendable bound for an away channel
|
||||
// before any sink sees it. the sinks pick Summary themselves too, but this is
|
||||
// where the boundary actually is: a sink added later must not be able to leak
|
||||
// the full body just by reading the wrong field.
|
||||
func minimalForAway(s Sendable) Sendable {
|
||||
if !isAway(s.Channel) {
|
||||
return s
|
||||
}
|
||||
msg := messageForChannel(s)
|
||||
s.Body = msg
|
||||
s.Summary = msg
|
||||
return s
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user