Add pluggable inbound-webhook framework + Granola meeting summarizer#4
Add pluggable inbound-webhook framework + Granola meeting summarizer#4Miyamura80 wants to merge 5 commits into
Conversation
Introduces webhooks/ as a place to mount custom webhook listeners on the same Bun.serve instance. First source is Granola: when a meeting wraps, the emitter POSTs to /api/webhooks/granola and a Codex-generated summary is streamed into the configured Slack channel. - Shared X-Webhook-Secret auth and Redis NX/EX dedup per source - Handler returns 200 immediately and processes in the background - Failures post a⚠️ notice to the same channel so issues are visible - Extracted streamCodex into lib/codex.ts so mentions and webhooks share it
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
There was a problem hiding this comment.
3 issues found across 6 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="webhooks/granola.ts">
<violation number="1" location="webhooks/granola.ts:42">
P2: Validate payload types before property access; non-object JSON can currently throw and return 500.</violation>
<violation number="2" location="webhooks/granola.ts:65">
P2: Normalize `attendees` to an array before joining; current code can throw before the catch block.</violation>
</file>
<file name="lib/codex.ts">
<violation number="1" location="lib/codex.ts:35">
P2: Validate the Codex subprocess exit code before returning; non-zero exits are currently treated as success.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Fix all with cubic | Re-trigger cubic
Addresses three review issues from cubic on PR #4: - webhooks/granola.ts: reject non-object JSON bodies (null, arrays, primitives) before field access, instead of letting a TypeError bubble up as a 500. - webhooks/granola.ts: normalize `attendees` with Array.isArray before calling .join, so a malformed (non-array, non-empty) value can no longer throw outside the surrounding try/catch in the background task. - lib/codex.ts: await the subprocess and throw on non-zero exit so silent codex failures surface as errors instead of empty summaries. Callers (webhook handler, onNewMention) now see the failure and can react.
Capture the macOS deploy setup that was previously only tribal knowledge: - deploy/com.edison.assistant-bot.plist: LaunchAgent template (RunAtLoad + KeepAlive, direct bun exec, no secrets — env comes from .env). - docs/DEPLOY.md: install/manage commands plus two gotchas hit in practice: (1) a checkout on an external/removable volume needs Full Disk Access on the bun binary or launchd gets EPERM reading files (TCC); (2) the codex CLI hangs in dyld if its signing cert is revoked — upgrade to a clean build. - CLAUDE.md: link to the new deploy doc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deployment captured in-repo (
|
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Addresses two cubic review comments on 0bb7814: - deploy/com.edison.assistant-bot.plist: parameterize user-specific fields with __USER__ and __WORKDIR__ placeholders instead of hardcoding one operator's username in StandardOut/ErrPath, PATH, HOME, and WorkingDirectory. Label stays as com.edison.assistant-bot (reverse-DNS prefix refers to the org, not the local user). - docs/DEPLOY.md: install step now renders the template via sed so all placeholders get substituted in one command. Documents which fields are parameterized and why. - docs/DEPLOY.md: rewrite the revoked-cert diagnostic. The old command relied on `readlink -f` (GNU-only; BSD readlink on macOS errors with "illegal option -- f") and manually joined path segments in a way that double-nested inside the codex npm package. New command uses `find` under `npm root -g` to locate the arch-specific binary — BSD-safe, arch-agnostic (arm64 and x86_64), and resilient to whether npm dedups the platform packages flat or under codex/node_modules.
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Addresses two cubic review comments on 36b5fae: - docs/DEPLOY.md: the previous "if deploying for a different user, replace $USER with the target username" note was misleading — the surrounding commands still targeted the current user's ~, $(id -u), and log paths. Replace it with a directive to run the whole install sequence as the target user so every reference resolves consistently. - docs/DEPLOY.md: the codex signing-cert diagnostic used the interactive shell's `npm root -g`, which nvm/nodenv/asdf can shadow. The bot spawns bare `codex` under the LaunchAgent's fixed PATH, so the diagnostic must resolve npm through that same PATH — otherwise it may inspect a different codex install than the one the bot actually runs, giving a false negative on a revoked cert. Rewrite to reproduce the bot's PATH and resolve npm via `command -v` under it (BSD-safe, arch-agnostic).
Summary
Two things: a pluggable inbound-webhook framework (with the first source, a Granola meeting summarizer), and the macOS deployment work needed to actually run it as a managed service.
1. Generic webhook framework
Each source is a factory
(ctx: { bot }) => { path, handler }exported fromwebhooks/<name>.ts.index.tsmounts every factory underBun.serve({ routes }). Adding a new source = drop a file + one line inindex.ts.Shared conventions (documented in
CLAUDE.md):X-Webhook-Secret: $WEBHOOK_SECRET.SET key NX EX 604800(7-day TTL) — retried deliveries return 200 without re-processing.:warning:notice to the same Slack channel so issues are visible instead of silently swallowed.2. Granola source
POST /api/webhooks/granolaaccepts{ meeting_id, title, transcript, notes?, attendees? }, asks Codex for a Slack-formatted wrap-up (header + 3-5 bullets + optional action items), and streams it intoGRANOLA_SLACK_CHANNEL_ID(#edison-os-updates) viabot.channel("slack:Cxxx").post(streamCodex(...)).Refactor: extracted
streamCodexout ofindex.tsintolib/codex.tssoonNewMentionand webhook handlers share one implementation, and it now awaits the subprocess and throws on non-zero exit instead of silently yielding an empty summary.3. Deployment (launchd)
The bot now runs as a launchd-managed service — auto-starts, auto-restarts on crash, survives reboot.
deploy/com.edison.assistant-bot.plistis a checked-in template (no secrets; env comes from.envinWorkingDirectory), anddocs/DEPLOY.mdcovers install/manage plus two non-obvious gotchas that cost real debugging time:read()-ing file contents on external/removable volumes —lsworks,read()returnsEPERM. Symptom is a PID with an empty log and nothing listening on 3123. Fix is granting FDA to thebunbinary.CSSMERR_TP_CERT_REVOKEDand stalled in_dyld_startbeforemain()on every invocation, including--version.npm install -g @openai/codex@latest(0.144.0) fixes it. Includes thespctl --assessdiagnostic.Files
webhooks/types.ts—WebhookContext,WebhookRoute,WebhookFactorywebhooks/granola.ts— Granola handlerlib/codex.ts— sharedstreamCodexindex.ts— wires the webhooks array intoBun.serve({ routes })deploy/com.edison.assistant-bot.plist— LaunchAgent templatedocs/DEPLOY.md— install, management, and the two gotchas above.env.example— addsWEBHOOK_SECRETandGRANOLA_SLACK_CHANNEL_IDCLAUDE.md— documents the inbound-webhook conventions + linksdocs/DEPLOY.mdSetup
.env:brew services start redis).docs/DEPLOY.md.Test plan
POST /api/webhooks/slackandPOST /api/webhooks/granola.X-Webhook-Secretreturns401.200immediately and a streamed wrap-up lands in#edison-os-updates.meeting_idreturns200 Duplicate (already processed)and does not post a second message.400.bunx tsc --noEmitis clean.KeepAliveretries while the external volume mounts.@mentionflow still streams a Codex reply (regression check on thestreamCodexextraction) — not yet verified, see below.Known follow-ups (not in this PR)
@mentionpath is unverified. The Slack app's Request URL still needs pointing at the current public endpoint for both Event Subscriptions and Interactivity & Shortcuts, with Socket Mode off.X-Webhook-Secretis a static, replayable bearer compared non-constant-time, and the JSON body has no size cap. Should move to HMAC-over-body + timestamp.Generated by Claude Code