Skip to content

Commit 712a3f5

Browse files
authored
feat(chat): support Matrix and Mattermost standalone (#12)
## Added - Matrix or Mattermost can run as the only chat platform; Slack is no longer required to enter chat mode. - Scheduled insight posts now go to every configured chat platform (Slack, Matrix, Mattermost), not just Slack. - Matrix and Mattermost notifiers retry transient failures (HTTP 429, 5xx, and common network errors), matching Slack's existing behavior. - New `InsightSink` abstraction (`src/notify/insightSink.ts`) and `RetryNotifier` (`src/notify/retryNotifier.ts`) with unit tests. ## Changed - `MATRIX_DEFAULT_ROOM_ID` is required whenever any Matrix env is set. It is used for scheduled insight posts and auto-join on startup, matching how Slack and Mattermost handle their channel. - Helm chart: Matrix and Mattermost non-sensitive config now sits under `config.*` and their tokens under `secrets.*`, matching the Slack pattern. Slack token emission is now conditional, so a Matrix-only or Mattermost-only deploy no longer ships empty Slack env vars. - Helm chart: removed env vars the app never read (`KAYTOO_OUTPUT`, `POLL_INTERVAL_SECONDS`, `DEDUPE_TTL_SECONDS`, `EGRESS_*`, `PORTSCAN_*`, `LOG_INCLUDE_DEBUG_BODIES`, `LOG_REDACT`, `MATRIX_SDK_LOG_LEVEL`). Output mode is auto-derived from which chat adapter envs are present. - Helm chart: default image tag resolves to `v{Chart.AppVersion}` so it matches the GHCR semver tag convention out of the box. - Logger reads `NODE_ENV` directly from `process.env`; the `nodeEnv` field on `LoggingInit` is gone. - README: chat adapter section is platform-neutral and links to each adapter's setup. Releases section rewritten as a numbered flow. - Cleaned up non-ASCII punctuation in source comments, tool descriptions, and test names. Test regex that intentionally rejects smart punctuation keeps its semantics via Unicode escapes. ## Fixed - Config validation no longer demands Slack env when only Matrix or only Mattermost is configured.
1 parent bb42dea commit 712a3f5

24 files changed

Lines changed: 644 additions & 252 deletions

README.md

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
> ⚠️ **Notice**
2-
> This repo is a small, experimental project built for fun and learning. Its shared publicly **as-is** with no promised support, roadmap, or guarantees.
2+
> This repo is a small, experimental project built for fun and learning. It's shared publicly **as-is** with no promised support, roadmap, or guarantees.
33
44
<div align="center">
55
<img src="assets/kaytoo.png" alt="Kaytoo logo" width="180" />
@@ -98,13 +98,11 @@ Kaytoo is configured via environment variables. For a minimal example, see `.env
9898

9999
#### Environment variables
100100

101-
**Slack** (required for chat mode. See [Slack setup (Socket Mode)](#slack-setup-socket-mode) below)
101+
**Chat adapters** - configure at least one to run in chat mode. Kaytoo posts scheduled insights to every configured platform and answers chat replies on the same platform a message arrives from. See the per-platform setup and full variable tables below:
102102

103-
| Variable | Required | Default | Notes |
104-
| --- | --- | --- | --- |
105-
| `SLACK_BOT_TOKEN` | yes | - | Bot token (`xoxb-...`). |
106-
| `SLACK_APP_TOKEN` | yes | - | App-level token for Socket Mode (`xapp-...`). |
107-
| `SLACK_CHANNEL_ID` | yes | - | Channel for scheduled insight posts. |
103+
- [Slack](#slack): `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`, `SLACK_CHANNEL_ID`
104+
- [Matrix](#matrix): `MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN`, `MATRIX_DEFAULT_ROOM_ID`
105+
- [Mattermost](#mattermost): `MATTERMOST_URL`, `MATTERMOST_TOKEN`, `MATTERMOST_CHANNEL_ID`
108106

109107
**Search backend** (Choose either OpenSearch or Elasticsearch. Do not set both)
110108

@@ -205,7 +203,7 @@ Uses [`matrix-js-sdk`](https://github.com/matrix-org/matrix-js-sdk) with an in-m
205203
| --- | --- | --- | --- |
206204
| `MATRIX_HOMESERVER` | yes | - | Homeserver base URL. |
207205
| `MATRIX_ACCESS_TOKEN` | yes | - | Access token for the bot user. |
208-
| `MATRIX_DEFAULT_ROOM_ID` | no | - | If set, Kaytoo attempts to join on startup. |
206+
| `MATRIX_DEFAULT_ROOM_ID` | yes | - | Room for scheduled insight posts. Kaytoo also attempts to join it on startup. |
209207

210208
##### Setup
211209

@@ -224,8 +222,8 @@ Uses [`matrix-js-sdk`](https://github.com/matrix-org/matrix-js-sdk) with an in-m
224222
- Invite the bot user into each room where Kaytoo should respond.
225223
- Kaytoo auto-joins rooms it is invited to.
226224

227-
5. **(Optional) default room**:
228-
- Set `MATRIX_DEFAULT_ROOM_ID` if you want Kaytoo to attempt to join a specific room on startup.
225+
5. **Default room**:
226+
- Set `MATRIX_DEFAULT_ROOM_ID` to the room that should receive scheduled insight posts. Kaytoo also attempts to join this room on startup.
229227

230228
6. **Run Kaytoo** with OpenSearch/Elasticsearch + LLM configured (same as Slack).
231229

@@ -235,7 +233,7 @@ Notes:
235233

236234
#### Mattermost
237235

238-
Inbound messages use the Mattermost WebSocket API (`/api/v4/websocket`); outbound replies use REST.
236+
Inbound messages use the Mattermost WebSocket API (`/api/v4/websocket`); outbound replies use REST. `MATTERMOST_URL`, `MATTERMOST_TOKEN`, and `MATTERMOST_CHANNEL_ID` enable chat mode.
239237

240238
| Variable | Required | Default | Notes |
241239
| --- | --- | --- | --- |
@@ -322,7 +320,16 @@ helm upgrade --install kaytoo ./helm/kaytoo \
322320

323321
#### Releases
324322

325-
Pushes to `main` run [`elastiflow/gha-reusable` `prepare-release`](https://github.com/elastiflow/gha-reusable), which may open a pull request that bumps `helm/kaytoo/Chart.yaml`, `package.json`, and prepends `CHANGELOG.md`. **Squash-merge that PR** so the resulting commit subject is exactly `[release vX.Y.Z] (#NNN)` (GitHub’s default squash title matches the PR title). Merging with that message triggers the GitHub release, container push to `ghcr.io/<owner>/<repo>`, and a Helm chart GitHub release plus `cr index` publish to the `gh-pages` branch when GitHub Pages is enabled for that branch.
323+
Releases are driven by [`elastiflow/gha-reusable`](https://github.com/elastiflow/gha-reusable) `prepare-release`. The flow:
324+
325+
1. **Push to `main`** runs `prepare-release`, which may open a PR that:
326+
- bumps `helm/kaytoo/Chart.yaml` and `package.json`
327+
- prepends a new entry to `CHANGELOG.md`
328+
2. **Squash-merge that PR.** Keep GitHub's default squash title so the commit subject is exactly `[release vX.Y.Z] (#NNN)`.
329+
3. **The release commit** then triggers:
330+
- a GitHub release for `vX.Y.Z`
331+
- a container push to `ghcr.io/<owner>/<repo>`
332+
- a Helm chart GitHub release plus `cr index` publish to the `gh-pages` branch
326333

327334
#### Useful scripts
328335

e2e/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Local stack: **Mermin** + OpenSearch ([`k8s/mermin-stack-values.yaml`](k8s/mermi
1313
| `npm run e2e:logs` | Tail Kaytoo in-cluster |
1414
| `npm run e2e:dev` | Run Kaytoo on the host against forwarded OpenSearch |
1515

16-
**Host Helm is not required** the bootstrap Job runs Helm in-cluster.
16+
**Host Helm is not required** - the bootstrap Job runs Helm in-cluster.
1717

1818
## Stack
1919

@@ -46,9 +46,9 @@ Created during `e2e:up` (and by `npm run e2e:llm-overlay` for the LLM file only)
4646

4747
| Artifact | Role |
4848
| --- | --- |
49-
| `kubeconfig-kind-kaytoo-e2e` | Host / `kind` CLI API server `https://127.0.0.1:` |
50-
| `kubeconfig-kind-kaytoo-e2e.docker` | Docker `kubectl port-forward` sidecars same cluster, API host `host.docker.internal` |
51-
| `kaytoo-values.rendered.yaml` | [`k8s/kaytoo-values.yaml`](k8s/kaytoo-values.yaml) + real `OPENSEARCH_PASSWORD` copied to node as `kaytoo-values.yaml` (**secrets**) |
49+
| `kubeconfig-kind-kaytoo-e2e` | Host / `kind` CLI - API server `https://127.0.0.1:...` |
50+
| `kubeconfig-kind-kaytoo-e2e.docker` | Docker `kubectl port-forward` sidecars - same cluster, API host `host.docker.internal` |
51+
| `kaytoo-values.rendered.yaml` | [`k8s/kaytoo-values.yaml`](k8s/kaytoo-values.yaml) + real `OPENSEARCH_PASSWORD` -> copied to node as `kaytoo-values.yaml` (**secrets**) |
5252
| `values-e2e.llm.local.json` | LLM Helm `-f` from `.env` via `e2e/write-e2e-llm-overlay.mjs` |
5353

5454
Legacy `e2e/.kubeconfig-kind-*` at the `e2e/` root is moved into `.generated/` on first use.
@@ -60,10 +60,10 @@ npm run e2e:up
6060
```
6161

6262
1. kind cluster `kaytoo-e2e` (prompts if it already exists).
63-
2. `docker build` `kaytoo-e2e:local` `kind load`.
63+
2. `docker build` -> `kaytoo-e2e:local` -> `kind load`.
6464
3. Writes `.generated/` (LLM JSON, rendered Kaytoo values), copies chart + those files to the control-plane node (`/kaytoo-chart/`), applies bootstrap Job.
65-
4. Job: metrics-server, Mermin stack, `helm upgrade -i kaytoo -f kaytoo-values.yaml -f values-e2e.llm.local.json`.
66-
5. Docker-based port-forwards: OpenSearch **9200**, Dashboards **5601**, chat **18080** `8080`.
65+
4. Job: metrics-server, Mermin stack, `helm upgrade -i kaytoo ... -f kaytoo-values.yaml -f values-e2e.llm.local.json`.
66+
5. Docker-based port-forwards: OpenSearch **9200**, Dashboards **5601**, chat **18080** -> `8080`.
6767

6868
**URLs after up:** OpenSearch `https://127.0.0.1:9200`, Dashboards `http://localhost:5601`, chat `http://127.0.0.1:18080`.
6969

helm/kaytoo/templates/_helpers.tpl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,3 @@ app.kubernetes.io/instance: {{ .Release.Name }}
3030
{{- default "default" .Values.serviceAccount.name -}}
3131
{{- end -}}
3232
{{- end -}}
33-

helm/kaytoo/templates/configmap.yaml

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ metadata:
55
labels:
66
{{- include "kaytoo.labels" . | nindent 4 }}
77
data:
8-
KAYTOO_OUTPUT: {{ .Values.kaytooOutput | quote }}
98
{{- if .Values.httpChat.enabled }}
109
KAYTOO_HTTP_CHAT_BIND: {{ printf "0.0.0.0:%d" (.Values.httpChat.port | int) | quote }}
1110
{{- end }}
11+
{{- if .Values.config.slack.channelId }}
1212
SLACK_CHANNEL_ID: {{ .Values.config.slack.channelId | quote }}
13+
{{- end }}
14+
1315
OPENSEARCH_URL: {{ .Values.config.opensearch.url | quote }}
1416
OPENSEARCH_TLS_INSECURE: {{ .Values.config.opensearch.tlsInsecure | quote }}
1517
OPENSEARCH_INDEX_PATTERN: {{ .Values.config.opensearch.indexPattern | quote }}
@@ -20,36 +22,22 @@ data:
2022
LLM_BASE_URL: {{ .Values.config.llm.baseUrl | quote }}
2123
LLM_MODEL: {{ .Values.config.llm.model | quote }}
2224

23-
POLL_INTERVAL_SECONDS: {{ .Values.config.behavior.pollIntervalSeconds | quote }}
24-
DEDUPE_TTL_SECONDS: {{ .Values.config.behavior.dedupeTtlSeconds | quote }}
25-
26-
EGRESS_MULTIPLIER: {{ .Values.config.thresholds.egressMultiplier | quote }}
27-
EGRESS_MIN_BYTES: {{ .Values.config.thresholds.egressMinBytes | quote }}
28-
PORTSCAN_PORTS_THRESHOLD: {{ .Values.config.thresholds.portscanDistinctDstPorts | quote }}
29-
PORTSCAN_MIN_PACKETS: {{ .Values.config.thresholds.portscanMinPackets | quote }}
30-
3125
LOG_LEVEL: {{ .Values.logging.level | quote }}
32-
LOG_INCLUDE_DEBUG_BODIES: {{ .Values.logging.includeDebugBodies | quote }}
33-
LOG_REDACT: {{ .Values.logging.redact | quote }}
34-
MATRIX_SDK_LOG_LEVEL: {{ .Values.logging.matrixSdkLevel | quote }}
35-
{{- if .Values.logging.nodeEnv }}
36-
NODE_ENV: {{ .Values.logging.nodeEnv | quote }}
37-
{{- end }}
3826

39-
{{- if and .Values.matrix.homeserver .Values.matrix.accessToken }}
40-
MATRIX_HOMESERVER: {{ .Values.matrix.homeserver | quote }}
41-
MATRIX_ACCESS_TOKEN: {{ .Values.matrix.accessToken | quote }}
42-
{{- if .Values.matrix.defaultRoomId }}
43-
MATRIX_DEFAULT_ROOM_ID: {{ .Values.matrix.defaultRoomId | quote }}
27+
{{- if .Values.config.matrix.homeserver }}
28+
MATRIX_HOMESERVER: {{ .Values.config.matrix.homeserver | quote }}
4429
{{- end }}
30+
{{- if .Values.config.matrix.defaultRoomId }}
31+
MATRIX_DEFAULT_ROOM_ID: {{ .Values.config.matrix.defaultRoomId | quote }}
4532
{{- end }}
4633

47-
{{- if and .Values.mattermost.url .Values.mattermost.token .Values.mattermost.channelId }}
48-
MATTERMOST_URL: {{ .Values.mattermost.url | quote }}
49-
MATTERMOST_TOKEN: {{ .Values.mattermost.token | quote }}
50-
MATTERMOST_CHANNEL_ID: {{ .Values.mattermost.channelId | quote }}
51-
{{- if .Values.mattermost.botUserId }}
52-
MATTERMOST_BOT_USER_ID: {{ .Values.mattermost.botUserId | quote }}
34+
{{- if .Values.config.mattermost.url }}
35+
MATTERMOST_URL: {{ .Values.config.mattermost.url | quote }}
36+
{{- end }}
37+
{{- if .Values.config.mattermost.channelId }}
38+
MATTERMOST_CHANNEL_ID: {{ .Values.config.mattermost.channelId | quote }}
5339
{{- end }}
40+
{{- if .Values.config.mattermost.botUserId }}
41+
MATTERMOST_BOT_USER_ID: {{ .Values.config.mattermost.botUserId | quote }}
5442
{{- end }}
5543

helm/kaytoo/templates/deployment.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ spec:
1919
{{- toYaml .Values.podSecurityContext | nindent 8 }}
2020
containers:
2121
- name: kaytoo
22-
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
22+
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default (printf "v%s" .Chart.AppVersion) }}"
2323
imagePullPolicy: {{ .Values.image.pullPolicy }}
2424
securityContext:
2525
{{- toYaml .Values.securityContext | nindent 12 }}

helm/kaytoo/templates/secret.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,18 @@ metadata:
77
{{- include "kaytoo.labels" . | nindent 4 }}
88
type: Opaque
99
stringData:
10+
{{- if .Values.secrets.slackBotToken }}
1011
SLACK_BOT_TOKEN: {{ .Values.secrets.slackBotToken | quote }}
12+
{{- end }}
13+
{{- if .Values.secrets.slackAppToken }}
1114
SLACK_APP_TOKEN: {{ .Values.secrets.slackAppToken | quote }}
15+
{{- end }}
16+
{{- if .Values.secrets.matrixAccessToken }}
17+
MATRIX_ACCESS_TOKEN: {{ .Values.secrets.matrixAccessToken | quote }}
18+
{{- end }}
19+
{{- if .Values.secrets.mattermostToken }}
20+
MATTERMOST_TOKEN: {{ .Values.secrets.mattermostToken | quote }}
21+
{{- end }}
1222
OPENSEARCH_USERNAME: {{ .Values.secrets.opensearchUsername | quote }}
1323
OPENSEARCH_PASSWORD: {{ .Values.secrets.opensearchPassword | quote }}
1424
LLM_API_KEY: {{ .Values.secrets.llmApiKey | quote }}

helm/kaytoo/values.yaml

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
replicaCount: 1
22

3-
# Passed to process env as KAYTOO_OUTPUT (slack | console). Console skips Slack / chat adapters.
4-
kaytooOutput: slack
5-
63
image:
7-
repository: kaytoo
8-
tag: "dev"
4+
repository: ghcr.io/elastiflow/kaytoo
5+
tag: ""
96
pullPolicy: IfNotPresent
107

118
serviceAccount:
@@ -33,50 +30,37 @@ resources:
3330
config:
3431
slack:
3532
channelId: ""
33+
matrix:
34+
homeserver: ""
35+
defaultRoomId: ""
36+
mattermost:
37+
url: ""
38+
channelId: ""
39+
botUserId: ""
3640
opensearch:
3741
url: ""
3842
tlsInsecure: "false"
39-
# Default matches ElastiFlow CODEX-style indices; use elastiflow-flow-* (or your alias) if your cluster uses a different pipeline/mapping. Must align with document fields Kaytoo resolves (see README).
43+
# Default matches ElastiFlow CODEX-style indices.
44+
# Use elastiflow-flow-* (or your alias) if your cluster uses a different pipeline/mapping.
45+
# This must align with document fields that Kaytoo resolves (see README).
4046
indexPattern: "elastiflow-flow-codex-*"
4147
mcpUrl: ""
4248
llm:
4349
baseUrl: ""
4450
model: "gpt-5.3-codex"
45-
behavior:
46-
pollIntervalSeconds: "300"
47-
dedupeTtlSeconds: "3600"
48-
thresholds:
49-
egressMultiplier: "3"
50-
egressMinBytes: "50000000"
51-
portscanDistinctDstPorts: "50"
52-
portscanMinPackets: "200"
5351

5452
secrets:
5553
existingSecret: ""
5654
slackBotToken: ""
5755
slackAppToken: ""
56+
matrixAccessToken: ""
57+
mattermostToken: ""
5858
opensearchUsername: ""
5959
opensearchPassword: ""
6060
llmApiKey: ""
6161

62-
# Optional adapters (auto-enabled if configured)
63-
matrix:
64-
homeserver: ""
65-
accessToken: ""
66-
defaultRoomId: ""
67-
68-
mattermost:
69-
url: ""
70-
token: ""
71-
channelId: ""
72-
botUserId: ""
73-
7462
logging:
7563
level: "info"
76-
includeDebugBodies: "false"
77-
redact: ""
78-
matrixSdkLevel: "WARN"
79-
nodeEnv: "" # non-empty → ConfigMap `NODE_ENV`; empty → keep image default
8064

8165
networkPolicy:
8266
enabled: false

src/agent/tools/definitions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export function buildToolDefinitionList(opts: {
104104
},
105105
{
106106
name: 'topConversations5Tuple',
107-
description: 'Top sourcedestination conversations by bytes using multi_terms on (srcIp,dstIp,srcPort,dstPort,proto).',
107+
description: 'Top source->destination conversations by bytes using multi_terms on (srcIp,dstIp,srcPort,dstPort,proto).',
108108
argsSchema: {
109109
type: 'object',
110110
properties: {
@@ -210,7 +210,7 @@ export function buildToolDefinitionList(opts: {
210210
},
211211
{
212212
name: 'namespaceEdgesByBytes',
213-
description: 'Top cross-namespace edges by bytes (source namespace destination namespace).',
213+
description: 'Top cross-namespace edges by bytes (source namespace -> destination namespace).',
214214
argsSchema: {
215215
type: 'object',
216216
properties: {

src/agent/tools/handlers/internalRfc1918DstFilter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function privateIpv4DstBool(dstIpField: string, opts?: { includeCgnat?: b
1919
};
2020
}
2121

22-
/** Internal / private destination IPs including CGNAT (for internal dst filters in aggregations). */
22+
/** Internal / private destination IPs including CGNAT (for "internal dst" filters in aggregations). */
2323
export function internalRfc1918DstIpFilter(dstIpField: string): Record<string, unknown> {
2424
return privateIpv4DstBool(dstIpField, { includeCgnat: true });
2525
}

0 commit comments

Comments
 (0)