Skip to content

feat(gateway): Complete implementation + fix Supermemory adapter#700

Merged
khaliqgant merged 4 commits into
mainfrom
feat/gateway-complete-memory-fix
Apr 9, 2026
Merged

feat(gateway): Complete implementation + fix Supermemory adapter#700
khaliqgant merged 4 commits into
mainfrom
feat/gateway-complete-memory-fix

Conversation

@khaliqgant
Copy link
Copy Markdown
Member

@khaliqgant khaliqgant commented Apr 8, 2026

Summary

  • Completes @agent-relay/gateway — router, rules engine, Slack/WhatsApp/Telegram adapters
  • Fixes @agent-relay/memory Supermemory filter format (was sending flat filters, now uses { AND: [{ key, value }] })

Gateway (packages/gateway/)

Types were already defined (172 lines). This PR adds the implementation:

  • router.ts — Gateway class: verify → parse → match rules → dispatch
  • rules-engine.ts — rule matching with priority sorting
  • adapters/slack.ts — Slack v0 HMAC, event parsing, Web API delivery
  • adapters/whatsapp.ts — Meta Cloud API, X-Hub-Signature-256
  • adapters/telegram.ts — Bot API, secret token header

Memory Fix (packages/memory/)

Supermemory adapter was sending:

{ "filters": { "agentId": "sage" } }

Now sends the correct format:

{ "filters": { "AND": [{ "key": "agentId", "value": "sage" }] } }

Consumers

  • Sage — uses gateway for Slack webhooks
  • NightCTO — replaces OpenClaw with gateway
  • MSD — will migrate to gateway in V4

Test plan

  • npm run build passes for gateway + memory packages
  • Gateway types compile with all adapters
  • Supermemory filter format matches API spec

🤖 Generated with Claude Code


Open with Devin

- Add Gateway router class (processWebhook, deliver, registerAdapter)
- Add rules engine (matchesRule, findMatchingRules, evaluateCondition)
- Add Slack adapter (v0 HMAC, app_mention/message parsing, chat.postMessage)
- Add WhatsApp adapter (X-Hub-Signature-256, Cloud API)
- Add Telegram adapter (secret token, Bot API)
- Fix @agent-relay/memory Supermemory adapter filter format
  (was: { agentId: 'x' }, now: { AND: [{ key: 'agentId', value: 'x' }] })

This is the shared gateway primitive for multi-platform conversational agents.
Open source — consumed by Sage, NightCTO, MSD.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@khaliqgant khaliqgant requested a review from willwashburn as a code owner April 8, 2026 19:49
Comment on lines +45 to +46
return text
.replace(/<@[A-Z0-9]+\|([^>]+)>/g, '@$1')
Comment thread packages/gateway/src/rules-engine.ts Fixed
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +74 to +75
case '!=':
return messageValue !== compareValue;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 != operator with null is not the logical negation of ==, causing contradictory rule matching

The == operator has special handling for null (treating undefined as equal to null at rules-engine.ts:70-71), but the != operator at line 75 uses plain messageValue !== compareValue with no corresponding special handling. When a field is missing (messageValue is undefined) and the condition compares against null:

  • $.field == nulltrue (via the special null handling)
  • $.field != nullundefined !== null → also true

Both a condition and its negation return true for the same message, which means rules using $.field != null to filter for messages where a field exists will incorrectly match messages where the field is absent.

Suggested change
case '!=':
return messageValue !== compareValue;
case '!=':
if (compareValue === null) {
return messageValue !== null && messageValue !== undefined;
}
return messageValue !== compareValue;
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved: The current code at rules-engine.ts:78-82 already contains the fix: the != operator now has special null handling (checking messageValue !== null && messageValue !== undefined), which is exactly what the suggested edit proposed. This bug has been resolved.

khaliqgant and others added 3 commits April 9, 2026 11:07
- Regenerate package-lock.json so @agent-relay/gateway is present,
  unblocking every `npm ci` step (lint, tests, install, e2e, coverage,
  codeql, licenses, npm audit, standalone smoke).
- Bump gateway to 4.0.5 and its @agent-relay/sdk dependency to 4.0.5
  to match the rest of the monorepo after the v4.0.5 release.
- Add `gateway` to the `publish-packages` matrix in publish.yml so the
  new package is published alongside the other workspaces on release.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The == operator treats undefined as equal to null (rules-engine.ts:70-71),
but != was a plain strict-inequality so `$.field == null` and `$.field != null`
both returned true when the field was absent — a condition and its negation
matching the same message.

Mirror the null-handling on !=: when comparing against null, return true
only if messageValue is neither null nor undefined.

Addresses Devin review comment on PR #700.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CodeQL js/polynomial-redos flagged the condition regex at rules-engine.ts:36.
Two unbounded \s* groups flanking an operator alternation caused O(n²)
backtracking on whitespace-heavy input like `\$.a\t\t...>` when the operator
failed to match.

Bound both whitespace regions to \s{0,16}. 16 chars of whitespace around an
operator is already pathological input — no legitimate rule uses that — and
the bound eliminates the polynomial backtrack while preserving ergonomics
for real rules.

A full parser rewrite (hand-rolled prefix-match, faster hot path, trivially
extensible) is tracked as a follow-up. This change is the minimum-scope fix
to clear the CodeQL finding in this PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 9 additional findings in Devin Review.

Open in Devin Review

verify(_payload: string, headers: HeaderMap): boolean {
const token = headers['x-telegram-bot-api-secret-token'] as string | undefined;
if (!token) return false;
return token === this.webhookSecretToken;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Telegram verify uses timing-unsafe === comparison for secret token

The Telegram adapter's verify method at line 44 uses a plain === string comparison for the webhook secret token, which is vulnerable to timing side-channel attacks. An attacker who can send requests to the webhook endpoint could potentially deduce the secret token character-by-character by measuring response time differences. Both the Slack adapter (packages/gateway/src/adapters/slack.ts:103) and the WhatsApp adapter (packages/gateway/src/adapters/whatsapp.ts:57) correctly use crypto.timingSafeEqual for their secret comparisons. The Telegram adapter doesn't even import crypto.

Prompt for agents
The Telegram adapter's verify method uses a plain === string comparison for the webhook secret token at line 44. This should use crypto.timingSafeEqual to be consistent with the Slack and WhatsApp adapters and to prevent timing side-channel attacks.

To fix:
1. Add import crypto from 'crypto' at the top of packages/gateway/src/adapters/telegram.ts (alongside the existing type imports)
2. Replace the === comparison with crypto.timingSafeEqual wrapped in try-catch (same pattern used in slack.ts:102-106 and whatsapp.ts:56-60):
   try { return crypto.timingSafeEqual(Buffer.from(token), Buffer.from(this.webhookSecretToken)); } catch { return false; }
The try-catch handles the case where the buffers have different lengths (timingSafeEqual throws in that case).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@khaliqgant khaliqgant merged commit aa408b5 into main Apr 9, 2026
32 of 33 checks passed
@khaliqgant khaliqgant deleted the feat/gateway-complete-memory-fix branch April 9, 2026 09:48
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