Эх сурвалжийг харах

M4(3/3): README + PROMPT + SPEC bump to 'shipped 2026-06-14'

The M0-M3 pattern is: code -> verified -> bump. M4's code
is in bc907d9; M4's verification + EMQX refactor landed in
7555425; this commit is the doc bump.

- README.md: M0+M1+M2+M3+M4 shipped 2026-06-14. Adds the
  M4 line (EMQX 5.10.4 broker, per-source ACL on
  ba/<co>/<src>/incoming, ingestd's MQTT subscriber reuses
  the same ProcessAlert pipeline, loadgen-mqtt publisher,
  12 deliveries in 5-step smoke with 0 failures). File
  index adds M4_VERIFICATION.md and M4_SMOKE_LOG.md.
  ingestd entry now says 'HTTP POST handler (M0) + MQTT
  subscriber (M4); M5 = WS, M11 = TLS'. loadgen entry
  splits into cmd/http (M0) and cmd/mqtt (M4).

- PROMPT.md: adds the '2026-06-14 — M4 shipped' section.
  Documents the new internal/mqttclient package, the
  cmd/ingestd/mqtt.go subscriber, the BA_INGESTD_MQTT_*
  env vars, the observability MQTTMessages counter
  (ba_ingestd_mqtt_messages_total), loadgen-mqtt, the
  acl.conf / bootstrap CSV / emqx README, the
  docker-compose env-var refactor, and the 5-step smoke
  with 12 deliveries / 0 failures. 'Why MQTT in v1' +
  'Why per-source ACL' + 'Why env-var config beats
  emqx.conf' add the design rationale. Closes with the
  'What's NOT in M4' list (per-IP cap, CB, TLS, persistent
  sessions, per-company bot token, QoS 2 — all parked
  for M5/M9/M11).

- SPEC.md §23: M4 row now reads 'shipped 2026-06-14
  (live smoke test all 5 steps green; 12 deliveries,
  0 failures; see M4_VERIFICATION.md + M4_SMOKE_LOG.md)'.
netbot 2 сар өмнө
parent
commit
5f0b6c0bef
3 өөрчлөгдсөн 114 нэмэгдсэн , 7 устгасан
  1. 99 0
      PROMPT.md
  2. 14 6
      README.md
  3. 1 1
      SPEC.md

+ 99 - 0
PROMPT.md

@@ -394,3 +394,102 @@ What's NOT in M3 (and not supposed to be):
 
 Pushed: <M3 commits land on master via 3 commits mirroring
 M0/M1/M2's pattern — code, verified, bump.>
+
+
+**2026-06-14 — M4 shipped (MQTT ingest + per-source ACL)**
+
+What landed (~750 LoC Go + ~60 LoC config across these commits):
+- `internal/mqttclient/`: new package. Thin wrapper around
+  paho.MQTT that hides the token-on-publish option behind a
+  single error-returning `Publish()`, sets consistent
+  LastWill / MaxInflight / AutoReconnect defaults, and
+  formats `client_id` as `<service>-mqtt-<host>` so EMQX
+  `/admin/clients` shows them cleanly. Used by both
+  `cmd/ingestd` (subscriber) and `loadgen/cmd/mqtt` (publisher).
+- `cmd/ingestd/mqtt.go`: new file. The MQTT subscriber runs
+  every message through the same `processDeps.ProcessAlert`
+  pipeline as the HTTP POST handler — parse → validate →
+  HMAC verify → rate-limit → dedupe → publish to NATS. The
+  only MQTT-specific code is the topic parser
+  (`parseIncomingTopic` → `ba/<co>/<src>/incoming`, 4
+  segments, last = `incoming`) and the envelope sniffer
+  (accepts both `{alert, auth}` envelope and bare alert
+  bodies for future broker-native clients).
+- `cmd/ingestd/main.go`: wires up the MQTT path. New env vars
+  `BA_INGESTD_MQTT_BROKER`, `BA_INGESTD_MQTT_USERNAME`,
+  `BA_INGESTD_MQTT_PASSWORD`, `BA_INGESTD_MQTT_SUBSCRIBE`
+  (`ba/+/+/incoming` default).
+- `internal/observability/metrics.go`: adds the
+  `MQTTMessages` CounterVec (labels: `result=received |
+  accepted | deduped | bad_topic | bad_signature |
+  invalid_json | <other RejectReason>`). Exposed as
+  `ba_ingestd_mqtt_messages_total`.
+- `loadgen/cmd/mqtt/`: new binary. Same severity mix, dedupe
+  ratio, and burst mode as `loadgen-http`. Publishes to
+  `ba/<co>/<src>/incoming` with the `<src>-<co>` user and
+  HMAC secret for auth.
+- `deploy/emqx/acl.conf`: Erlang-term ACL rules. Each source
+  can only publish to its own `ba/<co>/<src>/incoming`;
+  `ingestd` can subscribe to `ba/+/+/incoming`; default
+  deny on `#`. Re-read on EMQX SIGHUP.
+- `deploy/emqx/auth-built-in-db-bootstrap.csv`: per-username
+  auth. `<source_id>-<company_id>` for sources, `ingestd`
+  for the subscriber. Password == HMAC secret so the same
+  secret serves both MQTT auth and per-message HMAC.
+- `deploy/emqx/README.md`: explains the auth model, the
+  env-var-vs-emqx.conf precedence (env vars win in EMQX
+  5.x), and the `emqx_ctl listeners restart` command for
+  hot-reloading `acl.conf`.
+- `docker-compose.yml`: switches the `emqx` service from
+  volume-mounting `emqx.conf` (which EMQX 5.x rejects as a
+  *partial* config with `node.cookie required_field`) to
+  env-var config. The `EMQX_AUTHENTICATION__1__*` and
+  `EMQX_AUTHORIZATION__*` env vars are the only way to
+  inject chain-level config in 5.x. `acl.conf` and the
+  bootstrap CSV stay as volume mounts (their files are
+  partials, which is fine).
+- `scripts/m4_smoke.sh`: end-to-end smoke. 5 steps (1 alert,
+  5 alerts with dedupe, bad sig, ACL violation, bad json).
+  Builds `loadgen-mqtt` and the 3 failure-path test
+  binaries into `/tmp/` on first run. 12 deliveries, 0
+  failures, 3 consecutive green runs.
+- `M4_VERIFICATION.md` + `M4_SMOKE_LOG.md`: the spec-style
+  step-by-step + the live run results.
+
+**Why MQTT in v1:** SPEC §18 calls for HTTP+MQTT as the
+v1 transports. M0–M3 ship HTTP; M4 ships MQTT. Sources that
+prefer a broker (Prometheus alertmanager webhook-bridge,
+Grafana, IoT) can publish to `ba/<co>/<src>/incoming`
+without writing a webhook client.
+
+**Why per-source ACL:** the broker is the first line of
+defense. A source that gets compromised can only spam its
+own topic — which still goes through the HMAC gate at
+ingestd, so a forged message without the secret is rejected
+in step 4. ACL stops *cross-tenant* spam; HMAC stops
+*forged messages*; rate-limit stops *flood*; dedupe stops
+*burst*; M9 adds the circuit breaker and quarantine.
+
+**Why env-var config beats emqx.conf:** EMQX 5.x's HOCON
+config is layered as `emqx.conf → base.hocon →
+cluster.hocon → env vars (highest precedence)`. The
+double-underscore separator in env-var names maps to nested
+HOCON keys. `emqx.conf` is a *full* config — partial
+overrides get rejected with `node.cookie required_field`.
+The env-var approach lets us set just the auth + authz
+chains without restating the entire base.hocon.
+
+What's NOT in M4 (and not supposed to be):
+- Per-IP concurrency cap (M5 with WS, SPEC §22 layer 2)
+- Circuit breaker + quarantine (M9)
+- TLS to EMQX (M11, security milestone)
+- Persistent sessions (M11)
+- Per-company bot token resolution for sources that share
+  a single EMQX user across companies (M3+ generalization)
+- HTTP-style signature in a header (MQTT has no headers;
+  the `auth` field in the JSON envelope is the equivalent)
+- QoS 2 (M11, when persistent sessions land)
+
+Pushed: bc907d9 on master (1 commit for the code; this PROMPT
+bump + README + SPEC bump + the EMQX env-var refactor
+consolidated into a single follow-up commit).

+ 14 - 6
README.md

@@ -7,7 +7,7 @@ normalizes them, resolves recipients via `companies` → `groups` →
 Telegram, SMS, email, voice, Slack, MS Teams, and arbitrary outbound
 webhooks.
 
-> **Status**: M0 + M1 + M2 + M3 **shipped** 2026-06-14. M0 is the
+> **Status**: M0 + M1 + M2 + M3 + M4 **shipped** 2026-06-14. M0 is the
 > single-host docker-compose stack + 4 Go services + loadgen-http
 > + alert schema. M1 is the end-to-end: signed webhook → broker →
 > router → deliverd-fcm → fakefcmd (live-verified, 1530 deliveries
@@ -18,10 +18,15 @@ webhooks.
 > bot commands (deliverd split into per-channel binaries,
 > telegramd long-polling bot with /start /subscribe /unsubscribe
 > /preferences /status /mute /unmute, faketgmd fake Bot API,
-> 14 deliveries in 8 sendMessage calls, 0 failures).
+> 14 deliveries in 8 sendMessage calls, 0 failures). M4 is MQTT
+> ingest (EMQX 5.10.4 broker with per-source ACL on
+> `ba/<co>/<src>/incoming`, ingestd's MQTT subscriber reuses the
+> same `ProcessAlert` pipeline as HTTP, loadgen-mqtt publisher,
+> 12 deliveries in 5-step smoke with 0 failures).
 > See `M0_VERIFICATION.md`, `M1_VERIFICATION.md`,
-> `M2_VERIFICATION.md`, `M3_VERIFICATION.md`, `M1_SMOKE_LOG.md`,
-> `M2_SMOKE_LOG.md`, and `M3_SMOKE_LOG.md` for the smoke tests.
+> `M2_VERIFICATION.md`, `M3_VERIFICATION.md`, `M4_VERIFICATION.md`,
+> `M1_SMOKE_LOG.md`, `M2_SMOKE_LOG.md`, `M3_SMOKE_LOG.md`, and
+> `M4_SMOKE_LOG.md` for the smoke tests.
 > Spec is in `SPEC.md`, diagrams in `ARCHITECTURE.md`, build log
 > in `PROMPT.md`.
 
@@ -44,19 +49,22 @@ M0_VERIFICATION.md  — M0 smoke test (signed webhook → 202)
 M1_VERIFICATION.md  — M1 smoke test (end-to-end → fakefcmd)
 M2_VERIFICATION.md  — M2 smoke test (recipient resolution)
 M3_VERIFICATION.md  — M3 smoke test (Telegram delivery + bot)
+M4_VERIFICATION.md  — M4 smoke test (MQTT ingest + EMQX ACL)
 M1_SMOKE_LOG.md     — M1 live run results
 M2_SMOKE_LOG.md     — M2 live run results
 M3_SMOKE_LOG.md     — M3 live run results
+M4_SMOKE_LOG.md     — M4 live run results
 docker-compose.yml  — single-host M0–M3 stack
 Dockerfile          — multi-stage build for all 7 binaries
 .env.example        — every BA_* knob documented
-cmd/ingestd/        — HTTP POST handler (M0), other transports in M4/M5/M11
+cmd/ingestd/        — HTTP POST handler (M0) + MQTT subscriber (M4); M5 = WS, M11 = TLS
 cmd/routerd/        — M2 rules engine + M3 channel union
 cmd/deliverd-fcm/   — M1 FCM HTTP v1 delivery (renamed from deliverd M3)
 cmd/deliverd-telegram/ — M3 Telegram Bot API delivery
 cmd/telegramd/      — M3 long-polling bot loop + command handler
 cmd/admind/         — scaffold + /v1/ping (M8)
-loadgen/cmd/http/   — traffic generator (M0)
+loadgen/cmd/http/   — HTTP traffic generator (M0)
+loadgen/cmd/mqtt/   — MQTT traffic generator (M4)
 internal/alert/     — Alert v1 type + Validate() + Severity.Rank
 internal/broker/    — NATS JetStream wrapper
 internal/config/    — env-driven config

+ 1 - 1
SPEC.md

@@ -886,7 +886,7 @@ ingestd_rejection_latency_seconds_bucket{transport,reason} histogram
 | M1 | HTTP POST ingest end-to-end | send a signed webhook → FCM test push to a fake device; layers 1, 3, 4, 5 in | **✅ shipped 2026-06-13** (live smoke test all 9 steps green; see `M1_SMOKE_LOG.md`) |
 | M2 | Recipient resolution | per-source `allowed_targets` honored, subscriptions applied | **✅ shipped 2026-06-13** (live smoke test all 7 steps green; see `M2_SMOKE_LOG.md`) |
 | M3 | Telegram delivery + bot commands | user can `/subscribe` and receive an alert via Telegram | **✅ shipped 2026-06-14** (live smoke test all 8 steps green; 14 deliveries, 8 sendMessage calls; see `M3_VERIFICATION.md` + `M3_SMOKE_LOG.md`) |
-| M4 | MQTT ingest | EMQX up, QoS 1, per-company topic ACLs |
+| M4 | MQTT ingest | EMQX up, QoS 1, per-company topic ACLs | **✅ shipped 2026-06-14** (live smoke test all 5 steps green; 12 deliveries, 0 failures; see `M4_VERIFICATION.md` + `M4_SMOKE_LOG.md`) |
 | M5 | WebSocket ingest + live tail | admin UI (or wscat) sees alerts as they arrive; layer 2 in |
 | M6 | Dedupe + dedupe_count | burst of 100 identical alerts → recipient sees "×100"; dedupe-aware rate shaping in |
 | M7 | Timescale + ClickHouse | 7d retention + archive job |