screenOutbound runs the recipient gate + (when outbound_scan='on') the content scan over the composed body, and combines them into one applied action. Pure w.r.t. storage — the caller persists/annotates based on the verdict.
(ctx context.Context, agent *identity.AgentIdentity, req outbound.SendRequest)
| 163 | // scan over the composed body, and combines them into one applied action. Pure |
| 164 | // w.r.t. storage — the caller persists/annotates based on the verdict. |
| 165 | func (a *API) screenOutbound(ctx context.Context, agent *identity.AgentIdentity, req outbound.SendRequest) outboundVerdict { |
| 166 | var v outboundVerdict |
| 167 | |
| 168 | gateAction := piguard.ActionAllow |
| 169 | if flagged, addr := recipientGate(agent, req); flagged { |
| 170 | gateAction = piguard.Action(agent.OutboundPolicyAction) |
| 171 | v.gateFlagged = true |
| 172 | v.GateAddr = addr |
| 173 | } |
| 174 | |
| 175 | scanAction := piguard.ActionAllow |
| 176 | if agent.OutboundScan == identity.ScanOn && a.screen != nil { |
| 177 | body := composeScanBody(req) |
| 178 | segs, sig, _ := piguard.Extract(body, 0) |
| 179 | agg := a.screen.Evaluate(ctx, piguard.Request{ |
| 180 | Direction: piguard.DirectionOutput, |
| 181 | Segments: segs, |
| 182 | Signals: sig, |
| 183 | Sender: agent.EmailAddress(), |
| 184 | SizeBytes: len(body), |
| 185 | }) |
| 186 | act := agg.Action(agent.OutboundScanReviewThreshold, agent.OutboundScanBlockThreshold) |
| 187 | if act != piguard.ActionAllow { |
| 188 | scanAction = act |
| 189 | score := agg.Score |
| 190 | v.ScanScore = &score |
| 191 | v.scanDetected = true |
| 192 | v.Reason = scanReasonOutbound(agg) |
| 193 | for _, c := range agg.Categories { |
| 194 | v.Categories = append(v.Categories, c.Name) |
| 195 | } |
| 196 | v.catsJSON, _ = json.Marshal(agg.Categories) |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | v.scanAction = scanAction |
| 201 | v.Applied = piguard.MoreSevere(gateAction, scanAction) |
| 202 | |
| 203 | // Attribute the verdict to its driving producer for the denorm + event. |
| 204 | switch { |
| 205 | case v.scanDetected && scanAction == v.Applied: |
| 206 | v.ReviewReason = identity.ReviewReasonOutboundScan |
| 207 | case v.gateFlagged: |
| 208 | v.ReviewReason = identity.ReviewReasonRecipientGate |
| 209 | case v.scanDetected: |
| 210 | v.ReviewReason = identity.ReviewReasonOutboundScan |
| 211 | } |
| 212 | if v.Reason == "" && v.gateFlagged { |
| 213 | v.Reason = "recipient not permitted by outbound policy" |
| 214 | } |
| 215 | return v |
| 216 | } |
| 217 | |
| 218 | // screeningEvents builds the append-only audit rows for this verdict, keyed to |
| 219 | // messageID. Deterministic ids + ON CONFLICT DO NOTHING make a retried send |