feat(watchdog): Sentry flags + quarantine on above-band Jev verdicts (#44)

This commit is contained in:
Smokey committed 2026-10-28 09:00:38 -06:00
1 parent 66cdd053f8
commit 084a4309d4
4 files changed
+198 -7

No files matched your search

+81 -2
View File
@@ -170,6 +170,13 @@ func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) {
httpError(w, http.StatusBadRequest, fmt.Sprintf("%q is not a kind crows may post.", body.Kind))
return
}
// Watchdog enforcement (E3): a crow the Sentry flagged is silenced until
// the owner lifts the quarantine. The owner and the service roles never
// pass through this gate.
if body.Author != "owner" && !slices.Contains(serviceRoles, body.Author) && s.p.Ledger.IsQuarantined(body.Author) {
httpError(w, http.StatusForbidden, body.Author+" is quarantined pending the owner's decision.")
return
}
// Tagging (A1, EVENTS.md): Jev first, header fallback, say as last resort.
// A validated ask_owner header bypasses the classifier: Jev's kind
@@ -273,6 +280,11 @@ func (s *Server) triggerResponse(nest Nest, record Message) {
if record.Kind == "deliver" {
go s.checkDivergence(nest, record)
}
// Sentry watchdog (E3): watch every crow claim/deliver that answers a request.
if slices.Contains(nest.Crows, record.Author) && (record.Kind == "claim" || record.Kind == "deliver") {
go s.checkWatchdog(nest, record)
}
}
var modePostable = map[string]bool{"solo": true, "conspiracy": true, "split": true}
@@ -362,15 +374,19 @@ func (s *Server) respondOpenRequest(nest Nest, req Message) {
}
}
// maySpeakUnprompted is the trust gate for open (unaddressed) work.
// maySpeakUnprompted is the trust gate for open (unaddressed) work. A crow the
// Sentry quarantined (E3) stays silent regardless of its level.
func (s *Server) maySpeakUnprompted(crow string) bool {
return s.p.Ledger.Level(crow, "other") > Stranger
return s.p.Ledger.Level(crow, "other") > Stranger && !s.p.Ledger.IsQuarantined(crow)
}
// conspiracyLine is the lens directive added to every invited crow's prompt.
const conspiracyLine = "You are one lens among several on this request; answer from your own strength, not the flock's consensus."
func (s *Server) respondToRequest(nest Nest, req Message) {
if s.p.Ledger.IsQuarantined(req.To) {
return // a quarantined crow waits for the owner (E3)
}
s.respondOne(nest, req, req.To, false)
}
@@ -491,6 +507,69 @@ func (s *Server) checkDivergence(nest Nest, m Message, budgets ...*Budget) {
}
}
// checkWatchdog is the Sentry's off-task check (E3, JEV_STRATEGIES strategies
// 4 and 8): one Jev call per crow claim or deliver against the request it
// answers. A verdict at or above the band posts a flag and quarantines the
// crow. Owner messages are never inspected; at most one flag per crow per
// referent; quarantine sticks until the owner lifts it (ask_owner lands in
// #45).
func (s *Server) checkWatchdog(nest Nest, m Message) {
if m.Author == "owner" || m.RefersTo == "" {
return
}
target, found := s.msgs.ByMsgID(m.RefersTo)
if !found {
return
}
for _, x := range s.msgs.All(nest.ID) {
if x.Kind != "flag" {
continue
}
if flagged, ok := s.msgs.ByMsgID(x.RefersTo); ok && flagged.Author == m.Author && flagged.RefersTo == m.RefersTo {
return // this crow is already flagged on this expectation
}
}
var earlier []string
for _, d := range s.msgs.ReportsTo(nest.ID, m.RefersTo, "deliver") {
if d.ID != m.ID {
earlier = append(earlier, d.Text)
}
}
state := map[string]any{"request": target.Text, "crow": m.Author, "message": m.Text, "earlier": earlier}
budget := &Budget{name: "watchdog", limit: s.p.G.TokenBudgetPerItem}
scores, ok := s.p.Flock.ClassifyWatch(s.ctx, state, budget)
if !ok {
return
}
thr := s.p.Flock.jev.threshold
answers, hasAnswers := scores["answers_request"]
if scores["states_fact_without_source"] < thr && scores["contradicts_earlier"] < thr &&
(!hasAnswers || answers > 1-thr) {
return // below band on every question: say nothing, do nothing
}
var parts []string
for _, q := range []string{"answers_request", "states_fact_without_source", "contradicts_earlier"} {
if v, has := scores[q]; has {
parts = append(parts, fmt.Sprintf("%s %.2f", q, v))
}
}
flag := Message{
NestID: nest.ID,
Kind: "flag",
Author: "Sentry",
Text: fmt.Sprintf("%s looks off-task: %s (request: %.60s)", m.Author, strings.Join(parts, ", "), target.Text),
To: "owner",
RefersTo: m.MsgID(),
TaggedBy: "harness",
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
if record, err := s.msgs.Append(flag); err == nil {
s.emitNestMessage(record)
}
s.p.Ledger.Quarantine(m.Author)
s.p.save()
}
// nestReply asks one crow once and reads its KIND header. ok means the reply
// was responsive (claim, deliver or decline).
func (s *Server) nestReply(system, prompt, crow string, budget *Budget) (kind, body string, ok bool) {