Skip to content

feat(webhook): declared watch policy — rules instead of muting humans - #199

Open
defangdevs wants to merge 1 commit into
masterfrom
feat/webhook-watch-policy
Open

feat(webhook): declared watch policy — rules instead of muting humans#199
defangdevs wants to merge 1 commit into
masterfrom
feat/webhook-watch-policy

Conversation

@defangdevs

Copy link
Copy Markdown
Owner

Closes #197. Companion to defangdevs/local-channels#15 (local-webhook 0.11.0, merged), which added the mechanism this PR writes policy into — per-subscription when/drop payload predicates, the event-agnostic shape the owner asked for on local-channels#14 after #13 settled that the upstream repo carries mechanism only.

Motivation

A standing watch spawns a session for every non-CI event from an unmuted sender, and ignore_senders is sender-granular — so the only way to stop a human's close/merge echoes was to mute the human, losing their new issues as collateral. This box does exactly that today: its dispatch watch mutes both defangdevs (the box) and lionello (the human owner), so a human-filed issue spawns no triage session. Echo spawns also eat hook-session slots that drop at the cap instead of queueing (#170), so they starve real failures.

What this adds

  • services.agent-box.webhook.watchPolicy — declarative per-topic rules (when/drop/ignoreSenders/note), enforced onto the matching filter.dispatch.json entry by an ExecStartPre on each user's receiver daemon. Upgrade-only: the module governs watches sessions created; it never creates one, so the default matches nothing on a box without such a watch. The declaration replaces the managed fields wholesale (config and state must not drift); runtime fields (ttl, timestamps) stay the entry's own. Every start logs what was governed — or that the policy is idle — because a rule that silently applied to nothing reads like a rule that worked (Standing watches go silently inert once the hook-session cap is hit #170).
  • Default policy for the box's own-repos watch (github:defangdevs/*): spawn for issues/PRs opened/reopened by anyone but the box's login, and for terminal CI failures (failure, timed_out, action_required, startup_failure, stale, error) whoever triggered the run. Closes, merges, pushes, green/in-flight CI spawn nothing. Sender policy lives inside the rules, so the governed entry's ignoreSenders is cleared — deploying this un-mutes lionello, and their new issues get triaged again while their close buttons stay quiet.
  • Pin bump to local-channels 74a99dd (0.11.0). Supersedes the pin half of chore(webhook): pin local-webhook 0.10.1 so green CI stops spawning sessions #196; its green-run VM test stays valid, because a rule-less watch keeps the 0.10.1 failures-only brake unchanged.
  • Docs: agent-box-webhook --help and the built-in AGENTS.md now cover --when/--drop and the governed-entry rule (change the NixOS config, not the entry — the entry's note says so).
  • VM test (tests/webhook.nix): a second watch with declared policy gets its rules and loses its ad-hoc sender mute on daemon restart while the rule-less first watch is untouched; a signed close-echo delivery is declined and the decline is journaled; an outsider's opened issue still spawns a hook session.

Deployment note

Nothing here restarts anything by itself — the rules and the un-mute both land when the box is next switched (the daemon restart runs the reconcile, and the new pinned webhook.py evaluates the rules; there is no version skew window because both arrive in the same switch). Until then the live watch keeps today's behavior, mutes included. Sessions load the plugin from the marketplace clone tracking local-channels main, so they already speak 0.11.0; only the daemon runs the pin.

Checks run

  • nix run .#assemble (module regenerated; module-generated-up-to-date passes)
  • nix build .#checks.aarch64-linux.{module-generated-up-to-date,multi-user,module-single-file,webhook-route,download-route} — all pass natively (VM tests are x86_64-only and not runnable on this box)
  • tests/webhook.nix: nix-instantiate --parse + the ty/ruff testscript gates
  • The reconcile jq was exercised against a fixture mirroring this box's live filter.dispatch.json (entry with ignoreSenders: ["defangdevs","lionello"]): rules applied, mute cleared, timestamps/ttl preserved, sibling entries untouched. The governed entry was then driven through the pinned webhook.py 0.11.0 itself: human's opened issue → spawn; box echo, PR close, merge push, green run → no spawn; box's own failing run → spawn.

Refs: #197 (closes), #196 (pin superseded), #170 (cap wedge / logging), #192, local-channels#13/#14/#15.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SDHb5tyTqZwzKM6Qdr8cxv

…#197)

A standing watch spawned a session for every non-CI event from an
unmuted sender, and ignore_senders was the only lever — sender-granular,
so stopping a human's close/merge echoes meant muting the human, which
also swallowed the new issues the watch exists to catch. This box mutes
its owner today for exactly that reason, and echo spawns eat hook-
session slots that drop at the cap (#170).

local-webhook 0.11.0 (local-channels#15, spun out of local-channels#14
after #13 settled that the upstream repo is mechanism only) adds per-
subscription when/drop payload predicates: an entry carrying them owns
its whole spawn policy — the failures-only CI brake steps aside, sender
muting moves inside the rules where it can be scoped to echo-shaped
events, and every declined event is logged.

This PR is the policy half:

- webhook.watchPolicy option: declarative rules per topic, enforced onto
  the matching filter.dispatch.json entry by an ExecStartPre on each
  user's receiver daemon. Upgrade-only: the module governs watches that
  sessions created, it never creates one, so the default is inert on a
  box without such a watch. The declaration replaces the managed fields
  (when/drop/ignoreSenders/note) wholesale; runtime fields (ttl,
  timestamps) stay the entry's own.
- Default policy for the box's own-repos watch (github:defangdevs/*):
  spawn for issues/PRs opened or reopened by anyone but the box's own
  login, and for terminal CI failures whoever triggered the run;
  closes, merges, pushes, green and in-flight CI spawn nothing. The
  ignoreSenders on the governed entry is CLEARED — deploying this
  un-mutes the human owner, whose new issues then get triaged again.
- Pin bump to local-channels 74a99dd (0.11.0), superseding the pin half
  of #196 (its green-run VM test remains valid: rule-less watches keep
  the 0.10.1 failures-only brake).
- CLI + built-in AGENTS.md documentation for --when/--drop and the
  governed-entry rule (edit the NixOS config, not the entry).
- VM test: a governed watch gets its rules and loses its ad-hoc sender
  mute on daemon restart while a rule-less sibling entry is untouched; a
  close echo is declined (and the decline logged); an outsider's opened
  issue still spawns.

Verification:
- nix run .#assemble; checks.aarch64-linux module-generated-up-to-date,
  multi-user, module-single-file, webhook-route, download-route all
  pass natively.
- tests/webhook.nix passes nix-instantiate --parse and the ty/ruff
  testscript gates (the VM test itself is x86_64-only; not run here).
- The reconcile jq was exercised against a fixture mirroring this box's
  live filter.dispatch.json (entry with ignoreSenders
  ["defangdevs","lionello"]): rules applied, mute cleared, timestamps/
  ttl preserved, sibling entries untouched. The governed entry was then
  routed through the pinned webhook.py 0.11.0 directly: human's opened
  issue spawns, box echo / PR close / merge push / green run do not,
  box's own failing run does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDHb5tyTqZwzKM6Qdr8cxv
@defangdevs

Copy link
Copy Markdown
Owner Author

CI note: the failing check is vm-test-run-agent-box-memory-protection, the pre-existing red-master breakage from 7b687b9 (/var/lib/agent-box-sites/agent in ReadWritePaths with web off) that #198 fixes — not this PR. The webhook VM test, including the new watchPolicy section, passed in the same run (vm-test-run-agent-box-webhook> (finished: run the VM test script, in 55.33 seconds)). Once #198 lands I'll rebase/re-run so this goes green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Standing watches spawn a session for terminal non-CI events (PR/issue closed, branch deleted)

2 participants