Channel stack ownership (products/gateway vs monorepo providers)¶
Status: Time-bounded two-stack exception through 2026-10-31. Native Durable adapters remain the default; decommission is not selected by the current ADR. Security body: Done — shared verify kernels + rate-limit primitives in @open-cowork/shared/node (JOE-934 / post-#958/#959) Decision: adr/channel-stack-policy.md; telemetry contract: channel-stack-telemetry.md
Disposition (post-#959 / post-JOE-994)¶
| Layer | Status | Meaning |
|---|---|---|
| Security body (signature verify, Meta/Discord/Telegram/Slack kernels, rate-limit algorithm) | Done | Shared; dual-fix checklist still required for security PRs |
Protocol / adapter body (Durable channels/* vs monorepo gateway-provider-*) | Opt-in façades + bounded exception | Telegram native monorepo; Discord/WhatsApp monorepo bridge. Defaults remain Durable native through the ADR evidence window. |
Two stacks (intentional)¶
| Stack | Location | Consumers |
|---|---|---|
| Monorepo providers | packages/gateway-provider-* + packages/gateway-channel | apps/channel-gateway, apps/standalone-gateway |
| Durable Gateway channels | products/gateway/src/channels/* | cowork-gateway daemon only |
These stacks share product concepts (Telegram/WhatsApp/Discord) but must not be casually dual-fixed. Security and protocol bugs need an explicit owner.
The exception is owned by Gateway & Channels maintainers and expires on 2026-10-31. The ADR defines the representative telemetry window, quantitative removal thresholds, missing-data behavior, and immediate security/maintenance triggers. It supersedes the prior indefinite freeze wording.
Ownership matrix¶
| Change type | Fix in | Do not |
|---|---|---|
| Cloud Channel Gateway delivery | monorepo providers | products/gateway channels |
| Standalone Gateway providers | monorepo providers | products/gateway channels |
| Durable Gateway (cowork-gateway) inbound/outbound chat | products/gateway channels | monorepo providers (unless migrating) |
| Shared crypto / rate-limit / retry primitives | packages/gateway-channel or @open-cowork/shared | copy-paste into either stack |
Protocol composition boundary¶
The opt-in composition façades and shared inbound policy do not change the default owner: native Durable adapters remain the operator path. The dated channel-stack ADR owns any convergence decision; do not treat the retained protocol stacks as incomplete dual-stack security work.
Non-security capability keys are shared through packages/shared/src/channel-protocol-contract.ts. Capability declarations remain covered by tests/channel-protocol-dual-stack-contract.test.ts and the package conformance helpers under packages/gateway-channel/src/. Inventory guard: node scripts/check-channel-protocol-inventory.mjs. It fails closed on missing stack roots or decision sources without requiring the monorepo path to become the default.
Protocol stack façades¶
| Channel | Setting | Monorepo meaning |
|---|---|---|
| Telegram | channels.telegram.protocolStack / OPEN_COWORK_TELEGRAM_PROTOCOL_STACK | Native grammy provider (gateway-provider-telegram) |
| Discord | channels.discord.protocolStack / OPEN_COWORK_DISCORD_PROTOCOL_STACK | Webhook bridge (gateway-provider-discord); needs bridgeDeliveryUrl + bridgeSharedSecret |
channels.whatsapp.protocolStack / OPEN_COWORK_WHATSAPP_PROTOCOL_STACK | Webhook bridge (gateway-provider-whatsapp); needs bridge URL + secret |
Defaults are durable (native Durable adapters). Env overrides config for rollback/canary.
Durable product policy (trust allowlists, claims, denial probes) is shared via channels/channel-inbound-policy.ts on all stacks. Security kernels remain in @open-cowork/shared/node.
Native adapter decommission is not selected during the ADR's bounded evidence window. Until a reviewed convergence decision, the dual-stack security checklist remains required for channel security PRs.
Shared security kernel (2026-07-21)¶
Native platform webhook verify and rate-limit kernels live in @open-cowork/shared/node:
channel-webhook-security.ts:verifyMetaHubSignature256/verifyMetaHubVerifyToken(WhatsApp/Meta),verifyDiscordInteractionSignature(Discord),verifyTelegramWebhookSecretToken(Telegram secret-token header),verifySlackRequestSignature(Slack Events/Interactions)webhook-rate-limiter.ts: fixed-windowWebhookRateLimiter(Durable façade; monorepogateway-channelkeeps an algorithm twin for package boundaries)
Regression guard: scripts/check-dual-channel-security.mjs.
Dual-stack security checklist (required on channel security PRs)¶
Any PR that changes channel security or protocol (webhook signature verify, SSRF/callback URL policy, bearer/HMAC compares, rate limits, trusted-target allowlists, credential redaction for channel diagnostics) must:
- Identify which stack owns the bug (matrix above).
- Check the other stack for the same class of defect.
- Prefer fixing shared primitives in
packages/gateway-channelor@open-cowork/sharedwhen both stacks need the behavior. - Tick the dual-channel checklist in
.github/pull_request_template.md.
Do not land a security fix in only one stack without an explicit “other stack N/A / follow-up” note in the PR body.
CI gate (JOE-932)¶
On pull_request to master, the monorepo CI workflow runs:
with the PR body and the changed-file list. The gate is inactive (exit 0) for unrelated monorepo PRs.
Activates when the PR touches any of:
| Surface | Paths |
|---|---|
| Durable channels | products/gateway/src/channels/** |
| Monorepo providers | packages/gateway-provider-*/**, packages/gateway-channel/**, apps/channel-gateway/**, apps/standalone-gateway/** |
| Shared security kernels / guards | packages/shared/src/node/channel-webhook-security.ts, packages/shared/src/node/webhook-rate-limiter.ts, scripts/check-dual-channel-security.mjs, this doc, PR template |
How to satisfy
- Tick N/A when the change is not channel security/protocol, or
- Tick the stack(s) reviewed and Both stacks fixed (or note single-stack ownership / follow-up in Notes), or
- Put
Dual-stack checklist: exemptin Notes with a one-line rationale (intentional single-stack protocol work, docs-only ownership wording, etc.).
Local dry-run:
OPEN_COWORK_CHANGED_FILES=$'products/gateway/src/channels/whatsapp.ts' \
OPEN_COWORK_PR_BODY="$(cat <<'EOF'
- [x] N/A — not a channel security/protocol change
EOF
)" \
node scripts/check-dual-channel-pr-checklist.mjs
Kernel wiring regressions (copy-paste HMAC / Ed25519) remain covered by scripts/check-dual-channel-security.mjs via pnpm boundaries:check.