Skip to content

Commit fa5d965

Browse files
feat(connector-slack): Slack Web API connector + connector/channel reconciliation (ADR-0022) (#1418)
* docs(adr-0021): revise after implementation scan — engine join gap, Cube reconciliation, naming, task list A four-area code scan (spec/runtime/frontend/examples) found three things that change the plan: - Runtime != schema: groupBy/aggregations execute (single-object), but joins/having/windowFunctions are schema-only and NOT executed by IDataEngine or the SQL driver. The headline "revenue by account.region" cannot run via IDataEngine today — joins are the gating gap. - A parallel semantic layer already exists and is implemented (IAnalyticsService + CubeSchema); its NativeSQLStrategy is the only cross-object JOIN path but bypasses RLS/tenant. Don't build a third layer — reuse/compile-to Cube. - Naming collisions: Dataset/Dimension/Metric are already taken. Corrects the false "engine already does joins" premise, adds the Cube-reconciliation + naming decisions (D-A/B/C), and appends an implementation-scan section with a workstream task list and rough sizing (~6-8 weeks across both repos; rendering lives in sibling repo objectui). * feat(connector-slack): Slack Web API connector + connector/channel reconciliation (ADR-0022) Add ADR-0022 reconciling the two seams that both claim 'Slack': Connector (integration mechanism, built) vs MessagingChannel (human-notification layer, ADR-0012/0013, draft). Decision: notify-a-human => MessagingChannel; raw API call => connector_action; the channel delegates its transport to a Connector. Back-references added to ADR-0012 §2 and ADR-0013 §9. Add @objectstack/connector-slack — the second reference concrete connector (after connector-rest), validating the baseline connector registry and opening the ADR-0022 'raw API call' path. Static bot-token (bearer) auth only; OAuth2 install/refresh and credential vaulting stay enterprise. Actions: chat.postMessage, chat.update, and a generic call escape hatch. Surfaces Slack's logical 'ok' (HTTP 200 even on failure) without throwing. Tests: 8/8 (connector unit + end-to-end kernel boot dispatching a connector_action flow to slack.chat.postMessage). * docs(adr-0021): resolve D-A/B/C and Q1-Q3 on the AI-author / human-review criterion - D-A: dataset is the ONE author-facing semantic type; compiles to the existing Cube runtime; retire/absorb the author-facing Cube DSL. - D-B: take high-prior names dataset/measure/dimension; rename seed Dataset -> Seed; Cube Dimension/Metric absorbed (collision dissolves). - D-C: reuse Cube NativeSQLStrategy join path but make RLS + per-joined- object tenant scoping mandatory; v1 joins only along declared relations. - Q1: derived measures first-class now, CLOSED (reference measures by name only; no raw fields/SQL). - Q2: deferred; runtimeFilter only in v1. - Q3: deferred; conformed dimensions in a follow-up ADR. Rewrites D1 schema to a declarative surface smaller than QuerySchema (object + include relationships + dims/measures + derived), no raw SQL, joins derived from the object graph. Adds the six AI-author principles that drive the decisions. WS2 drops XL->L; estimate ~4-6 weeks. --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
1 parent 7692eb8 commit fa5d965

13 files changed

Lines changed: 975 additions & 23 deletions

docs/adr/0012-notification-platform.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ Every layer has a stable seam. A custom channel (e.g. `plugin-notification-feish
180180
181181
### 2. The `MessagingChannel` interface
182182
183+
> **Transport delegation (ADR-0022).** Per [ADR-0022](./0022-connectors-vs-messaging-channels.md), a channel's *transport* — provider auth, base URL, rate-limit handling, the provider's action set — should be implemented on top of a `Connector` (ADR-0015/0018), not hand-rolled per channel. `MessagingChannel` adds only the messaging *semantics* (preferences, inbox, outbox, sessions) on that substrate. The interface below is unchanged; this is an implementation guideline for concrete channels.
184+
183185
```ts
184186
// packages/spec/src/notification/channel.zod.ts
185187
export interface MessagingChannel {

docs/adr/0013-bidirectional-messaging.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,8 @@ The synthetic-user fallback is what keeps the system honest: nothing happens "as
483483

484484
### 9. Slack channel — first reference implementation
485485

486+
> **Transport delegation (ADR-0022).** The Slack channel's outbound `send()` (and any Web-API calls below) should delegate to a Slack `Connector` (e.g. `@objectstack/connector-slack`) for auth / base URL / rate-limit / the `chat.*` action set, rather than hand-rolling `fetch`. The channel adds the messaging semantics on top. See [ADR-0022](./0022-connectors-vs-messaging-channels.md) — this lets the `connector_action`-direct path and the channel share one Slack transport.
487+
486488
`@objectstack/plugin-messaging-slack`. Package layout:
487489

488490
```

docs/adr/0021-analytics-dataset-semantic-layer.md

Lines changed: 133 additions & 23 deletions
Large diffs are not rendered by default.
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# ADR-0022: Connectors vs Messaging Channels — Where "Push to Slack" Lives
2+
3+
**Status**: Proposed (2026-05-31)
4+
**Deciders**: ObjectStack Protocol Architects
5+
**Builds on**: [ADR-0012](./0012-notification-platform.md) (Messaging Platform — outbound `notify` on a generalized outbox), [ADR-0013](./0013-bidirectional-messaging.md) (Bidirectional Messaging — Slack first), [ADR-0015](./0015-external-datasource-federation.md) (open mechanism / enterprise lifecycle split), [ADR-0018](./0018-unified-node-action-registry.md) (`connector_action` as baseline generic dispatch)
6+
**Consumers**: `@objectstack/spec` (`integration/`, future `messaging/`), `@objectstack/services/service-automation` (connector registry), future `@objectstack/service-messaging`, future `@objectstack/plugin-messaging-slack`, `@objectstack/connectors/*`
7+
8+
---
9+
10+
## TL;DR
11+
12+
The question "should pushing a message to Slack be a **connector**?" surfaces a real collision in the repo: **Slack now has two candidate homes.**
13+
14+
- **ADR-0012/0013** designate Slack as a **`MessagingChannel`** — a human-facing notification + conversation surface with a preference matrix, an inbox, an outbox, sessions/threads, and an inbound receiver. *Both ADRs are Draft; none of `service-messaging`, the `notify` transport, or `plugin-messaging-slack` is built yet.*
15+
- **ADR-0015/0018 + the just-merged connector baseline** give us a **`Connector`** — a generic "call any external system's API" mechanism (`connector_action` node, `engine.registerConnector()`, `@objectstack/connector-rest`). *This is built and live.*
16+
17+
This ADR resolves the collision instead of letting two seams quietly grow a second Slack integration. The decision:
18+
19+
1. **A `Connector` is the transport/integration mechanism** — "talk to system X's API," no recipient semantics. A `MessagingChannel` is the **human-messaging semantic layer** — recipients, preferences, inbox, outbox, sessions, inbound. They are **different abstractions at different layers**, exactly as "SMTP transport" ≠ "send a notification" in ADR-0012.
20+
2. **"Notify a human via Slack" is NOT a connector** — it is a `MessagingChannel` (ADR-0012/0013). Modeling it as a bare `connector_action` would silently drop preferences, the inbox, retry/dead-letter, and the reply-in-thread story, and would **duplicate ADR-0012**.
21+
3. **"A flow makes a raw Slack API call" IS a connector action**`connector_action(connectorId:'slack', actionId:'chat.postMessage', …)`. This is the generic-integration path and works on **today's** baseline with no new abstraction.
22+
4. **The two share one transport.** A Slack `MessagingChannel.send()` should be **implemented on top of** a Slack `Connector` (auth, base URL, rate-limit, action set live once, in the connector); the channel adds the messaging semantics on top. Connector = mechanism; channel = policy. This generalizes ADR-0012's own "email becomes a transport sub-system" pattern.
23+
24+
Because ADR-0012/0013 are still Draft, folding the connector baseline in underneath them is a **cheap revision, not a migration**.
25+
26+
---
27+
28+
## Context
29+
30+
### The two seams, side by side
31+
32+
| Dimension | `Connector` (ADR-0015/0018, **built**) | `MessagingChannel` (ADR-0012/0013, **draft, unbuilt**) |
33+
|:---|:---|:---|
34+
| Purpose | call any external system's API | deliver to / converse with a **human** |
35+
| Primitive | an **action** (a verb on a system) | a **channel** (a delivery medium for a person) |
36+
| Recipient model | none — caller supplies explicit inputs | per-user preference matrix, mute, quiet hours, recipient resolver |
37+
| Delivery guarantee | one call → success/fail | outbox, retry, dead-letter, cluster-lock |
38+
| Threads / sessions | none | `sessionKey`, reply-to-thread, `sys_message_mirror` |
39+
| Inbound | generic webhook trigger | verify+dedup+parse → `target-resolver` → flow / hook / AI tool |
40+
| Multi-account | not modeled | `sys_channel_account`, first-class, multi-tenant |
41+
| Identity mapping | none | `sys_channel_user_link` (Slack user ↔ `sys_user`) |
42+
| Status today | **shipped** (`connector_action`, `@objectstack/connector-rest`) | **Draft** (`notify` is a no-op; `service-messaging` does not exist) |
43+
44+
They overlap on exactly one thing: the bytes on the wire eventually hit Slack's `chat.postMessage`. Everything *around* that call is different — and that difference is the whole reason ADR-0012/0013 exist.
45+
46+
### Why this is live now
47+
48+
The connector baseline landed *after* ADR-0012/0013 were drafted. When 0013 was written, a Slack channel plugin was expected to hand-roll its own `fetch`, auth, base URL, and rate-limit handling ([0013 §9](./0013-bidirectional-messaging.md)). We now have a baseline that provides exactly those things generically. So the honest re-evaluation is twofold:
49+
50+
1. **Routing question** — when a builder wants to "push to Slack," which seam do they reach for?
51+
2. **Substrate question** — now that connectors are baseline, should the (still-unbuilt) Slack channel be re-specified to *sit on top of* a connector rather than hand-roll transport?
52+
53+
### The precedent: this is the email split, generalized
54+
55+
ADR-0012 already drew this exact line for email: SMTP/Resend/Postmark are **transports** (`EmailTransport`), and "send a notification" is a **channel** (`MessagingChannel`) that *uses* a transport and adds preferences + outbox + suppression on top. `Connector` is simply the *general* form of "transport": a uniform way to talk to any external API. So the reconciliation is not a new idea — it is applying ADR-0012's own layering to every channel, with `Connector` as the shared transport substrate.
56+
57+
---
58+
59+
## Decision
60+
61+
### 1. Three concerns, three homes — do not collapse them
62+
63+
```
64+
┌────────────────────────────────────────────────────────────────────────┐
65+
│ CONVERSATION / INBOUND ADR-0013 │
66+
│ receiver · verify · sessions · target-resolver → flow/hook/AI tool │
67+
└───────────────────────────────┬──────────────────────────────────────────┘
68+
│ reuses
69+
┌───────────────────────────────▼──────────────────────────────────────────┐
70+
│ HUMAN NOTIFICATION ADR-0012 │
71+
│ notify node · recipient resolve · preference matrix · inbox · outbox │
72+
│ MessagingChannel.send() │
73+
└───────────────────────────────┬──────────────────────────────────────────┘
74+
│ delegates transport to
75+
┌───────────────────────────────▼──────────────────────────────────────────┐
76+
│ INTEGRATION MECHANISM ADR-0015 / 0018 (BUILT) │
77+
│ Connector · connector_action · auth · base URL · rate-limit · actions │
78+
└────────────────────────────────────────────────────────────────────────────┘
79+
```
80+
81+
- **Integration mechanism** (`Connector`, `connector_action`): "talk to any external API." Baseline, shipped. Slack-as-an-API-target is a legitimate connector.
82+
- **Human notification** (`MessagingChannel`, `notify`): "tell a person, honoring their preferences, with durable delivery." ADR-0012. **A connector is not a substitute** for this layer.
83+
- **Conversation/inbound** (sessions, resolvers, AI): ADR-0013. Built on the notification layer.
84+
85+
### 2. Routing rule — answer to "should push-to-Slack be a connector?"
86+
87+
| What the builder wants | Seam | Available |
88+
|:---|:---|:---|
89+
| "Notify the deal **owner** that the deal closed" (respect their channel preference, write the inbox, retry on failure, reply in the same thread) | **`MessagingChannel` + `notify` node** | when ADR-0012 ships |
90+
| "Post **this exact text** to **#ops**" / "look up a Slack user" / "create a channel" — a raw API step with no recipient/preference semantics | **`connector_action` on a Slack `Connector`** | **today** (needs a `connector-slack` plugin) |
91+
92+
The litmus test: **does a *person's preference* decide whether and where this message lands?** If yes → `MessagingChannel`. If the flow author has already decided the exact destination and just needs the API called → `connector_action`.
93+
94+
### 3. One transport — the channel delegates to the connector
95+
96+
A Slack `MessagingChannel.send()` SHOULD be implemented on top of a Slack `Connector`'s outbound action(s), so Slack auth, base URL, rate-limit handling, and the action set live in **one** place (the connector) and the channel adds only the messaging semantics (preference check, render, outbox row, session/thread decoding). This:
97+
98+
- removes the hand-rolled `fetch` that [0013 §9](./0013-bidirectional-messaging.md) currently assumes;
99+
- gives the connector path and the channel path a **single** Slack-auth/rate-limit implementation;
100+
- lets the same Slack `Connector` serve raw `connector_action` flow steps *and* back the channel.
101+
102+
This is the ADR-0012 "email = transport sub-system behind a channel" pattern, with `Connector` as the universal transport.
103+
104+
### 4. Open-source / enterprise boundary (consistent with ADR-0015)
105+
106+
| Capability | Tier |
107+
|:---|:---|
108+
| `Connector` contract + `connector_action` dispatch + in-process registry | **open** (shipped) |
109+
| Static-auth REST connector (`@objectstack/connector-rest`) | **open** (shipped) |
110+
| `MessagingChannel` interface + outbox + inbox + preference matrix + `notify` | **open baseline** — this is the #1292 P0 fix; it must be in the open framework |
111+
| A community Slack connector / channel using a **static bot token** (bearer) | **open** |
112+
| Managed OAuth2 **install + token refresh**, credential vault (`service-secrets`), multi-tenant `sys_channel_account` lifecycle, premium connectors, marketplace | **enterprise** (per ADR-0015; ADR-0013 leans on `service-secrets` + OAuth — those are the enterprise bits) |
113+
114+
A static Slack bot token is just a bearer credential, so a basic open-source Slack connector + channel is shippable in the framework; the managed-credential / OAuth-refresh / multi-tenant lifecycle is the enterprise line — the same split ADR-0015 drew for datasources and ADR-0018's addendum drew for connectors.
115+
116+
### 5. Anti-patterns this rules out
117+
118+
-**A generic "notification connector."** Notification is a *semantic layer above* connectors, not a connector. Do not invent one.
119+
-**Growing preference/inbox/outbox/session logic onto `Connector`.** That re-implements ADR-0012 in the wrong layer. The connector stays a dumb, generic API caller.
120+
-**A Slack channel plugin that hand-rolls its own `fetch`/auth** when a Slack connector already encapsulates it. One transport implementation, reused.
121+
-**Forcing `plugin-email` to become a connector right now.** It predates this, has its own template/transport story, and ADR-0012 already folds it in as `plugin-notification-email`. Leave it; revisit only if a generic transport buys something concrete.
122+
123+
---
124+
125+
## Consequences
126+
127+
**Positive**
128+
- One unambiguous answer to "where does push-to-Slack live," with a one-line litmus test builders can apply.
129+
- The unbuilt messaging stack gets a free, uniform transport substrate (the connector baseline) instead of N hand-rolled HTTP clients.
130+
- The open/enterprise line stays identical across datasources, connectors, and channels — one mental model.
131+
- No migration: ADR-0012/0013 are Draft, so this is a spec revision before code exists.
132+
133+
**Negative / costs**
134+
- ADR-0012/0013 need a small revision to say "the Slack channel's `send()` delegates to a Slack connector" (this ADR records the intent; the prose edits to 0012/0013 are follow-ups).
135+
- A Slack `Connector` and a Slack `MessagingChannel` both exist — two artifacts for one product. Mitigated by the strict layering (mechanism vs semantic) and the shared transport, so there is no duplicated Slack-auth code.
136+
- The `connector_action`-direct path can post to Slack *without* preference/inbox semantics. That is intended (it is the "raw API call" escape hatch), but docs must steer "notify a human" toward `notify`, not `connector_action`, so authors don't accidentally bypass mute/quiet-hours.
137+
138+
---
139+
140+
## Status of the surrounding ADRs (so scope is clear)
141+
142+
- **Built**: connector mechanism — `connector_action` baseline node, `engine.registerConnector()`, `@objectstack/connector-rest` (ADR-0018 addendum, merged 2026-05-31).
143+
- **Draft, unbuilt**: `service-messaging`, the `notify` transport, the preference matrix, the inbox, `plugin-messaging-slack` (ADR-0012/0013). The `notify` node is still a no-op.
144+
- **This ADR changes no code.** It records the layering decision and the routing rule so that when ADR-0012/0013 are implemented, the Slack channel is built *on* the connector substrate rather than beside it.
145+
146+
## Follow-ups (not in this ADR)
147+
148+
- Add a back-reference note to [ADR-0012 §2](./0012-notification-platform.md) and [ADR-0013 §9](./0013-bidirectional-messaging.md): "channel transport delegates to a `Connector` per ADR-0022."
149+
- A `connector-slack` plugin (open, static-token) — would validate the registry with a second connector and immediately enable the `connector_action`-direct path; lands independently of the messaging stack.
150+
- Revisit whether `plugin-email`'s transports should re-express as connectors once a second transport-heavy channel exists.

0 commit comments

Comments
 (0)