Skip to content

feat(webhook): per-user local-webhook receiver daemon (#101)#146

Draft
defangdevs wants to merge 1 commit into
masterfrom
feat/issue-101-local-channels
Draft

feat(webhook): per-user local-webhook receiver daemon (#101)#146
defangdevs wants to merge 1 commit into
masterfrom
feat/issue-101-local-channels

Conversation

@defangdevs

Copy link
Copy Markdown
Owner

Wires local-channels / local-webhook into agent-box as an opt-in, per-user webhook channel. Closes half of #101 (the other half — the receiver-daemon refactor of the plugin — is defangdevs/local-channels#2).

Design (as locked in #101)

Grain: per-user endpoint → per-user receiver daemon → per-session subscriptions (fan-out over IPC among that user's sessions).

  • Daemon: agent-box-webhook-<user> runs webhook.mjs with LOCAL_WEBHOOK_RECEIVER_ONLY=1, owning a socket-activated UNIX ingress at /run/agent-box-webhook/<user>.sock, bound 0660 <user>:caddy by a .socket unit — same isolation model as the settings socket (Settings daemon is unauthenticated on localhost: any local user can tamper with another agent's keys #49). No MCP session of its own.
  • Caddy: handle /<user>/webhook* reverse-proxies to that socket without basic_auth (GitHub can't authenticate; webhook.mjs's per-source HMAC is the trust boundary). More specific than /<user>/*, so no 401s for the fail2ban jail. Register https://<domain>/<user>/webhook in the provider; strip_prefix maps /<user>/webhook/github/github.
  • Sessions: the plugin is registered+enabled non-interactively via seeded ~/.claude/settings.json (extraKnownMarketplaces github source + enabledPlugins), and each claude session gets a per-session LOCAL_WEBHOOK_SESSION (+ LOCAL_WEBHOOK_STATE_DIR, LOCAL_WEBHOOK_PORT=0) exported at spawn so subscriptions never leak between sessions.
  • Pin: local-channels pinned by rev+hash via builtins.fetchTarball; nodejs-slim added for the daemon and (when enabled) the session PATH (the plugin's .mcp.json runs a bare node).

New option: services.agent-box.webhook.{enable,repo,rev,sha256}default off, so existing boxes are unaffected. Requires web.enable.

Source edited in modules/agent-box.nix.in; modules/agent-box.nix regenerated (assemble --check passes, nix-instantiate --parse clean).

⚠️ Draft — NOT yet tested end-to-end

Blocked on two things before it can be verified:

  1. defangdevs/local-channels must be public for the fetchTarball pin (currently private; my token can't flip it). The webhook.sha256 default is a placeholder to confirm on the first real fetch.
  2. A real nixos-rebuild + live claude session to confirm the channel loads via seeded settings (no trust prompt) and deliveries actually flow to a session.

Also worth a follow-up: an assertion that webhook.enable → web.enable.

🤖 Generated with Claude Code

Wires the local-channels local-webhook plugin into agent-box as an opt-in,
per-user webhook channel (services.agent-box.webhook.enable, default off):

- A receiver-only daemon per web user (agent-box-webhook-<user>) runs
  webhook.mjs in RECEIVER_ONLY mode, owning a socket-activated UNIX ingress
  (0660 <user>:caddy, same isolation model as the settings socket) and fanning
  HMAC-verified deliveries to that user's sessions over IPC.
- Caddy reverse-proxies the UNAUTHENTICATED public path /<user>/webhook to that
  socket (GitHub can't basic-auth; webhook.mjs's per-source HMAC is the trust
  boundary). More specific than /<user>/*, no basic_auth, no 401s.
- Sessions load the plugin non-interactively via seeded settings.json
  (extraKnownMarketplaces + enabledPlugins) and get a per-session
  LOCAL_WEBHOOK_SESSION (+ STATE_DIR, PORT=0 pure IPC peer) exported at spawn.
- local-channels pinned by rev+hash via builtins.fetchTarball; nodejs-slim
  added to the daemon and (when enabled) the session PATH for the plugin's node.

Source edited in agent-box.nix.in; agent-box.nix regenerated (assemble --check
passes). Default-off, so existing boxes are unaffected.

NOT YET TESTED end-to-end: needs defangdevs/local-channels public (the pin
fetch) plus a real nixos-rebuild + a live claude session to confirm the
channel loads and deliveries flow. The webhook.sha256 default is a placeholder
to confirm on first real fetch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx9PVLmDbzS35JCN5N85i6
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.

2 participants