-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathchangelog.html
More file actions
901 lines (853 loc) · 85.3 KB
/
Copy pathchangelog.html
File metadata and controls
901 lines (853 loc) · 85.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Changelog — Tiyi</title>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="description" content="Tiyi release notes from foundation work through v3.2.1." />
<meta name="theme-color" content="#0A0E1A" />
<link rel="icon" href="favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" />
<link rel="stylesheet" href="assets/css/main.css" />
<!-- Apply theme before paint to avoid FOUC -->
<script>(function () { try { var t = localStorage.getItem('tiyi:theme') || 'dark'; if (t !== 'dark' && t !== 'light') t = 'dark'; document.documentElement.dataset.theme = t; } catch (e) { document.documentElement.dataset.theme = 'dark'; } })();</script>
</head>
<body data-assets-base="assets/img/">
<div class="bg-field" aria-hidden="true"></div>
<header class="nav">
<div class="container nav-inner">
<a class="brand" href="index.html">
<img src="assets/img/tiyi-logo.png" alt="Tiyi logo" />
<span>Tiyi</span>
</a>
<nav class="nav-links" aria-label="Main navigation">
<a href="index.html#features">Features</a>
<a href="index.html#tour">Product</a>
<a href="index.html#pricing">Pricing</a>
<a href="docs/index.html">Docs</a>
<a href="changelog.html" style="color:var(--fg)">Changelog</a>
<a href="index.html#roadmap">Roadmap</a>
<button class="nav-toggle" type="button" data-action="toggle-theme" aria-label="Toggle theme">
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
</button>
<a class="nav-toggle nav-lang" href="zh/changelog.html" hreflang="zh-CN" aria-label="Switch to Chinese">中文</a>
<a class="btn btn-primary btn-sm nav-cta" href="index.html#pricing">Get Tiyi</a>
</nav>
</div>
</header>
<section style="padding: 64px 0 32px;">
<div class="container">
<div class="section-head" style="text-align:left; margin-bottom:32px;">
<span class="eyebrow">Changelog</span>
<h2 style="font-size: clamp(32px, 4vw, 48px);">Tiyi release history.</h2>
<p style="margin: 0;">Every entry maps back to a commit and a verification command. Items are <span class="tl-tag shipped" style="display:inline-block; vertical-align:middle;">Shipped</span> only after implementation, verification, and required docs are complete — otherwise <span class="tl-tag future" style="display:inline-block; vertical-align:middle;">Planned</span> or <span class="tl-tag current" style="display:inline-block; vertical-align:middle;">In progress</span>. Archived entries preserve the terminology and commands that existed at that time; use the newest entry and current documentation for supported operations.</p>
</div>
<div class="timeline"> <div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.5.4</span>
<span class="tl-date">2026-08-13</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Visual custom-rule HTTP data + event-scoped alert content</div>
<div class="tl-body">
<p>v3.5.4 lets operators write custom rules against recognizable HTTP data instead of Coraza collections, keeps recipe starters honest, and lets notification channels customize each lifecycle event without a firing template covering remediation receipts.</p>
<ul>
<li><strong>JSON field targeting.</strong> Exact JSON paths and the same field name at any nesting depth compile through server-owned <span class="mono">JSON_FIELD</span> / <span class="mono">JSON_FIELD_NAME</span> into phase-2 <span class="mono">ARGS:json.*</span> selectors. The builder shows Content-Type → processor → selector → phase; raw body is labelled unstructured, and an explicit JSON Content-Type + raw-body conjunction is rejected.</li>
<li><strong>Five Coraza-verified recipes.</strong> Temporary path-prefix block, noisy User-Agent fragment, required header below a path, exact method+endpoint disable, and exact query value below a path. Server-owned defaults apply across Web/API/CLI. Authorization redaction, Content-Length observability, and foreign-IP starters are retired because they cannot be guaranteed or already have first-class controls.</li>
<li><strong>Event-scoped channel templates.</strong> Override title/body per <span class="mono">firing</span>, <span class="mono">repeat</span>, remediation success/failure, automatic recovery, or operator closure. Missing events keep the built-in renderer. <span class="mono">PreviewAlertChannelContent</span> renders unsaved effective output without delivery. Top-level <span class="mono">titleTemplate</span> / <span class="mono">messageTemplate</span> are rejected.</li>
<li><strong>Bounded API inventory.</strong> Discovery uses a 4,096-key probation stage and a 10,000-key promoted dictionary with a 2,000-key protected reserve. Confirmed identities survive restart. The explorer capacity rail and <span class="mono">tiyi_api_inventory_value</span> gauges make pressure explicit; native deny, rate-limit, probation, overflow, and compaction stay as fixed aggregate rows.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.5.3</span>
<span class="tl-date">2026-08-10</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Native IP-list enforcement + incremental alert remediation</div>
<div class="tl-body">
<p>v3.5.3 moves country and CIDR IP-list enforcement out of SecLang into a compressed native matcher, so large <span class="mono">geo:CC</span> blocks can no longer truncate CRS, and keeps auto-remediation appending while an alert stays firing.</p>
<ul>
<li><strong>Native <span class="mono">tiyi_iplist</span>.</strong> Minimized prefixes compile into a deterministic compressed artifact matched before rate limiting and Coraza. Country CIDRs never enter SecLang; oversized policies fail visibly and keep the last-good configuration.</li>
<li><strong>SecLang completeness guard.</strong> Apply rejects physical Coraza lines over 48 KiB, broken Include graphs, or a missing terminal parse canary before local or Agent Caddy load.</li>
<li><strong>Bundle v3.</strong> Local/Agent config bundles carry the native IP-list artifact. This binary rejects retained v2 bundles; generate and apply a fresh revision.</li>
<li><strong>Incremental auto-remediation.</strong> While an occurrence remains firing, each evaluation may append the next five new qualifying attackers (≤50 unique IPs per occurrence) with per-IP reservation and fixed TTL batches; UI shows partial/progress receipts.</li>
<li><strong>Rule-owned deny lists.</strong> Auto-create reuses the managed list by alert-rule ID with locale-aware display names; deleting the rule releases the list as ordinary operator content.</li>
<li><strong>Rate-limit runtime bound.</strong> Per-policy token-bucket state is capped; unseen identities fail closed when the budget is full rather than granting an unbounded burst.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.5.2</span>
<span class="tl-date">2026-08-09</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Display timezone + clearer protection authoring</div>
<div class="tl-body">
<p>v3.5.2 hardens day-to-day operator UX on top of v3.5: one display-timezone chain, a clearer IP-list ownership model, safer RE2 custom-rule authoring, and a more readable audit ledger.</p>
<ul>
<li><strong>Display timezone.</strong> Header override → system-wide <span class="mono">app.timezone</span> → browser IANA. Tables, charts, relative presets, and custom ranges share the conversion path. API, SQLite, audit-chain, SIEM, and export timestamps remain UTC; process logs still follow the host timezone.</li>
<li><strong>IP Lists ownership.</strong> Protection → IP Lists owns shared list content with Create only / Create and apply. Policy IP access edits bindings only. The flow is Define a list → Apply to a scope → Query IP.</li>
<li><strong>RE2 workbench.</strong> Selecting the regex operator explains query-string inclusion, generates escaped exact/tree/prefix/suffix/case-insensitive patterns, warns on near-universal expressions, and rejects invalid RE2 (including PCRE lookaround/backreferences) before save.</li>
<li><strong>Audit Trail.</strong> Labeled filters, operator/resource names in the ledger, readable change summaries, cursor-based Load more, and permission-gated chain verification.</li>
<li><strong>CRS / limits polish.</strong> Shared PL1–PL4 discrete paranoia controls with blocking/log-only/inactive posture; Request limits separate parameter vs upload ceilings with explicit CRS-default vs override switches.</li>
<li><strong>Remediation receipts.</strong> Auto-remediation completion/failure notifications render concise action receipts instead of repeating the firing investigation snapshot.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.5.1</span>
<span class="tl-date">2026-08-06</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Fail-closed client-IP topology + intent-based rule drafting</div>
<div class="tl-body">
<p>v3.5.1 hardens the v3.5 executable client-IP topology and turns Attack Log rule drafting into an intent-based workflow that can skip CRS without disabling the rest of the WAF.</p>
<ul>
<li><strong>Header-owned extraction.</strong> Each Header independently chooses automatic verified-proxy skipping or fixed extraction (single IP, leftmost, rightmost, Nth-from-right). CDN and proxy ranges compile into one physical lookup pool while node labels and legal topology transitions stay intact.</li>
<li><strong>Fail-closed topology checks.</strong> Automatic extraction requires a terminal <span class="mono">RemoteAddr</span> on a proxy node connected to Tiyi, then scans right to left. Cross-branch hops, all-trusted chains, XFF paths through preserve/duplicate/unknown writers, and claimed upstream hops behind an overwrite writer reject that rule and continue to the next Header or <span class="mono">RemoteAddr</span> fallback.</li>
<li><strong>Intent-based Attack Log drafting.</strong> <span class="mono">Create rule from this event</span> groups actions as Block request, Tune CRS false positives, and High-risk CRS exceptions. Every item states query/descendant/method scope; root <span class="mono">/</span> cannot create path-scoped drafts.</li>
<li><strong>CRS-only skip outcome.</strong> Visual and seeded rules can compile <span class="mono">ctl:ruleRemoveByTag=OWASP_CRS</span> so OWASP CRS is skipped while Tiyi custom rules, IP controls, and other rule sets remain active. High-risk actions require an extra confirmation.</li>
<li><strong>Operator docs.</strong> QUICKSTART, PRDs, trust/API docs, and website EN/zh changelog and build stamps track the patch.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.5.0</span>
<span class="tl-date">2026-08-03</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Executable client-IP topology + safer protection authoring</div>
<div class="tl-body">
<p>v3.5.0 makes client-IP resolution an executable topology operators can sample, verify, and apply, turns dashboard security rankings into immediate response actions, and gives rule and endpoint-limit changes one deterministic path from evidence to enforcement.</p>
<ul>
<li><strong>Executable client-IP topology.</strong> Sites is the sole global entry. Full and quick editors show a live node/edge/rule graph with CDN/proxy pills, path-conditioned header rules, unsaved-preview status, and verified drafts that can apply to the source site or promote to the global default.</li>
<li><strong>Site analysis.</strong> Memory-only sampling (five minutes or 50 requests) distinguishes facts, range/path verification, advisory provider signatures, and conflicts before anything is saved. Provider-specific and generic headers may coexist on multi-CDN/stacked graphs because every rule is conditioned on its verified node path.</li>
<li><strong>Dashboard security insights.</strong> Six simultaneous SecurityFact ranking cards—including Attacked sites—plus an attack-origin country/region strip. Hover actions exclude ranks from the current view or globally block attacker IPs and <span class="mono">geo:CC</span> entries for 24 hours.</li>
<li><strong>Geo country compilation.</strong> The live DB-IP Country reader publishes an immutable Country→CIDR snapshot on boot/upload/refresh/delete; every policy compiler path consumes the same resolver so Dashboard/global country blocks compile cleanly.</li>
<li><strong>Canonical endpoint rate limits.</strong> Endpoint rows now have one source of truth across API, CLI, policy, site override, and compiled data plane. Paths are unique within policy or site scope, create and update are explicit, and a matching site row deterministically overrides the inherited policy row before CRS runs.</li>
<li><strong>Evidence-to-protection workflow.</strong> Attack Logs can draft exact-path, segment-bounded path-tree, method/path, IP/path, or retained User-Agent rules. Structured CRS evidence can instead seed a path-scoped target exclusion or rule disable; duplicate IP actions and whole-path WAF bypass seeds are removed.</li>
<li><strong>Draft-safe policy editing.</strong> Policy and site custom-rule editors use focused subviews, while site rate limits edit inline with field-level validation. Modified drafts require an explicit discard before navigation or drawer close.</li>
<li><strong>Operator surfaces.</strong> Product-tour screenshots refresh against a live multi-node instance and now include fleet topology and hash-chain audit; QUICKSTART, PRDs, and generated API docs track the clean-break client-IP contract.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.4.0</span>
<span class="tl-date">2026-07-30</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">One Controller, convergent configuration, operator trust</div>
<div class="tl-body">
<p>v3.4.0 makes the served state explicit from durable configuration through the operator UI, while removing the legacy HA contract.</p>
<ul>
<li><strong>Single Controller.</strong> <span class="mono">tiyi run</span> owns state, API, UI, and a factual built-in local data plane. Role, epoch, promotion, replication, standby, and alternate-Controller surfaces are removed; remote Agents continue serving their last accepted signed bundle during an outage.</li>
<li><strong>Convergent configuration.</strong> Durable desired/active revisions, immutable artifacts, local rollback, per-target delivery, content-addressed Agent envelopes, and contextual retry replace handler-local mutate/apply compensation.</li>
<li><strong>Health truth.</strong> Active upstreams pass a strict Controller-side submit probe before mutation. Continuous Caddy health is bound to the exact configuration generation and artifact hash, so stale reports cannot claim a candidate is active.</li>
<li><strong>Security and reliability.</strong> Generated fail-closed authorization, rotating HttpOnly refresh sessions, password token revocation, structured logging, correlated internal errors, panic-isolated workers, bounded shutdown, and pre-write quotas harden the management plane.</li>
<li><strong>Operator experience.</strong> Exact request/bandwidth dashboards, shared local/remote node trends, site request-path overviews, local configuration history, adaptive row actions, automatic Geo lifecycle, and security-only Request Evidence defaults make day-two state easier to read and safer to operate.</li>
<li><strong>Clean-break boundary.</strong> Databases below schema 47 are rejected without mutation; start v3.4.0 with fresh state and re-enroll remote Agents.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.3.1</span>
<span class="tl-date">2026-07-24</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Request evidence + producer-direct SIEM</div>
<div class="tl-body">
<p>v3.3.1 adds two optional, bounded observation planes and hardens long-running telemetry storage without moving any storage or network work into the WAF request path.</p>
<ul>
<li><strong>Request Evidence.</strong> Global and per-site <span class="mono">off</span>, <span class="mono">security_only</span>, and <span class="mono">retained_logs</span> policies retain unredacted parsed headers and bounded body bytes beside authorized Attack/Access log drawers; fresh installs remain off.</li>
<li><strong>Durable local and Agent storage.</strong> Append-only daily segments, quotas, retention, deterministic exemplar convergence, resumable 64 KiB CRC32C Agent uploads, and durable acknowledgements bound memory, disk, and reconnect behavior.</li>
<li><strong>Producer-direct SIEM.</strong> Up to 32 independent UDP/TCP/TLS destinations select native Caddy access JSON, native Coraza audit JSON, or Tiyi-formatted sources with per-target queues, framing, timeouts, and diagnostics. Signed Agent last-known-good profiles continue while the Controller is offline.</li>
<li><strong>Operational visibility.</strong> Log Pipeline exposes evidence, raw-source, and per-destination queue/drop/failure/panic lanes. Dashboard Top cards use exact normalized URL templates and fixed UA classes.</li>
<li><strong>Runtime hardening.</strong> Idle previous-day SQLite WALs are safely truncated behind partition lease fences, and Tiyi defaults to <span class="mono">GOGC=200</span> plus a soft memory limit at 90% of the detected host/container limit unless standard Go overrides are supplied.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.3.0</span>
<span class="tl-date">2026-07-22</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">SecurityFacts consolidation + alert auto-remediation</div>
<div class="tl-body">
The sessionized incident, attack-campaign, and MITRE-mapping layer is removed; immutable, partitioned SecurityFacts are now the sole security-event truth, with a fresh-install schema-43 cutover (no in-place upgrade from older state). Alert auto-remediation turns a firing alert into a reviewed IP-list or custom-rule draft; a durable fact journal and bounded Live Tail (≤200 rows, one event per 500 ms window) keep investigation fast. Operator docs, merge log, and website synchronized in EN + zh.
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.2.1</span>
<span class="tl-date">2026-07-17</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">v3.2.0 QA hardening + reliability</div>
<div class="tl-body">
Closes the v3.2.0 QA pass: unmatched Host headers return 421 instead of bypassing the WAF, CRS rule 980099 outbound correlation runs cleanly, and drawer, auto-close, and i18n defects are fixed. Reliability additions include SQLite BUSY retry, an orphan-evidence lookup index with a durable join worker, and bounded per-response network GeoIP enrichment.
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.2.0</span>
<span class="tl-date">2026-07-16</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Observation v2 + persistent attack campaigns</div>
<div class="tl-body">
<p>v3.2 turns request observation into an isolated, evidence-first pipeline and adds a durable campaign layer for correlating incidents without placing analytics in the data path.</p>
<ul>
<li><strong>Observation v2.</strong> Exact hot-path counters, compact <span class="mono">SecurityFact</span>/<span class="mono">Finding</span> evidence, retained request detail, and SIEM delivery are independent truth planes. Explicit global/per-site evidence policy and tiered retention replace legacy log settings.</li>
<li><strong>Durable agent delivery.</strong> Agent spool, ACK/retry, protocol fencing, partition management, and panic-isolated observation callbacks keep proxy traffic independent from log consumers and analytical workers.</li>
<li><strong>Persistent Attack Campaigns.</strong> Deterministic source/target, Geo/ASN, and MITRE projections feed an evidence graph and timeline with immutable revisions, conflict-safe triage, merge/split, membership, and edge review.</li>
<li><strong>API Inventory.</strong> Telemetry Explorer discovers endpoints with live QPS, status, and latency, then supports confirm, quarantine, hide, and block workflows.</li>
<li><strong>Actionable alerts.</strong> Localized alert titles and payloads include evidence URLs, target labels, campaign TTP context, and an explicit Geo/ASN caveat.</li>
<li><strong>Operator experience and docs.</strong> Updated EN/zh screenshots, campaign and incident workflows, plus task-based operations, troubleshooting, and upgrade guidance across the website and public repos.</li>
<li><strong>Clean-state boundary.</strong> Because there is no supported production upgrade population yet, pre-v3.2 development/test databases and agent identity/bundle state are rejected; reset and re-enrollment are explicit.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.1.0</span>
<span class="tl-date">2026-07-12</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Task-oriented navigation + accessible admin workspaces</div>
<div class="tl-body">
<p>v3.1.0 reorganizes the management plane around the jobs operators perform, then applies one responsive and accessible interaction system across the highest-traffic security workflows.</p>
<ul>
<li><strong>Seven task-oriented groups.</strong> Operations, Application Delivery, Protection, Fleet, Events & Logs, Detection & Response, and Administration replace resource-shaped menu silos without changing route URLs or RBAC permissions.</li>
<li><strong>Focused application shell.</strong> The desktop rail has one collapse model, concise labels, distinct hover/current states, and a compact header; mobile prioritizes navigation, search, theme, language, and identity while moving Copilot to a thumb-sized floating action.</li>
<li><strong>Keyboard-accessible navigation.</strong> Submenus expose expanded/current state, Enter and Space operation, focus-visible treatment, and reduced-motion support. Permission filtering still removes empty groups server-side.</li>
<li><strong>Consistent operator workspaces.</strong> Policy and site-security workspaces gain clearer overview/module navigation, unsaved-change protection, and reusable empty, diff, live-state, and YAML-apply patterns; alerting, incident response, certificates, agents, logs, settings, and list pages share the same visual and interaction language.</li>
<li><strong>Documentation contract.</strong> PRD-UI v3.9, the operator quickstart, and the admin-navigation design record the stable information architecture, responsive behavior, and accessibility acceptance criteria.</li>
<li>Verified: <span class="mono">go test ./...</span>, <span class="mono">buf lint</span>, <span class="mono">pnpm typecheck</span>, locale-key validation, production web build, and desktop/mobile Playwright QA.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.0.9</span>
<span class="tl-date">2026-07-10</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Visual-rule bypass action + responsive admin UI</div>
<div class="tl-body">
<p>v3.0.9 extends the visual rule builder with an explicit bypass action for trusted false-positive exceptions, and rolls out a responsive-width and copy-affordance pass across the admin UI.</p>
<ul>
<li><strong>Bypass WAF action.</strong> The visual builder's action set (block / log / pass) gains bypass, which compiles to <span class="mono">pass,nolog,ctl:ruleEngine=Off</span> so matching traffic skips the rest of WAF inspection. Plain pass is now <span class="mono">pass,nolog</span> (silent, passes only that rule; later CRS rules still run); block and log still audit-log.</li>
<li><strong>False-positive handoff.</strong> The security-event <em>Create rule from this event</em> menu adds <em>Allow this path / false positive</em>, which drafts a tightly scoped bypass rule scoped to the event's site.</li>
<li><strong>Responsive drawers and modals.</strong> Every Tiyi drawer and modal width now clamps to the viewport via a shared <span class="mono">useResponsiveWidth</span> helper, so wide drawers no longer overflow smaller screens.</li>
<li><strong>Markdown copy buttons.</strong> Fenced code blocks in AI and markdown output get a native, localized copy button (replacing click-to-copy on the whole block); <span class="mono">TiyiCopyButton</span> is localized.</li>
<li><strong>Recipe catalog launcher.</strong> The custom-rule builder's server template catalog moves behind a compact <span class="mono">Browse recipes</span> control so recipe browsing no longer splits the edit form; the static eight-preset selector remains as a fallback.</li>
<li><strong>Field-mask fix.</strong> Visual-spec custom-rule updates now send the correct protojson field-mask path (<span class="mono">visualSpec</span>), so edits to visual rules persist correctly.</li>
<li>Verified: <span class="mono">go test ./...</span>, <span class="mono">buf lint</span>, <span class="mono">pnpm --filter @vben/web-antd typecheck</span>, <span class="mono">python3 scripts/i18n_check.py</span>, and signed linux/amd64 + linux/arm64 release packaging.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.0.8</span>
<span class="tl-date">2026-07-09</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Policy enforcement hardening + doctor state-ownership repair</div>
<div class="tl-body">
<p>A focused hardening pass on v3.0.7: closes a rate-limit enforcement gap for sites running the inherited policy, tightens IP-list and CRS-scalar mutation semantics, and gives <span class="mono">tiyi doctor</span> a way to repair service-owned state directories.</p>
<ul>
<li><strong>Policy-wide rate limits enforced everywhere.</strong> Rate-limit endpoints were only merged into a site's compiled policy when a site overlay existed, so sites running the inherited policy silently skipped policy-wide limits. The merge now runs for every site, overlay or not.</li>
<li><strong>IP-list update_mask preserves kind.</strong> Updating an IP list with a field mask that omits <span class="mono">kind</span> no longer resets it; the stored kind is retained unless explicitly changed.</li>
<li><strong>CRS <span class="mono">enable_default_collections</span> tri-state.</strong> The scalar is now a proto3 optional so an explicit "off" survives protojson round-trips instead of collapsing back to the inherited default.</li>
<li><strong>Doctor state-ownership repair.</strong> <span class="mono">tiyi doctor</span> now inspects whether the state directory tree is writable by the configured service user, and <span class="mono">--fix-state-ownership</span> (root) chowns it back; custom supervisors are supported via <span class="mono">--service-user/--service-group</span>.</li>
<li><strong>Security-event handoff + UI refinements.</strong> "Create alert rule from this event" again resolves its target dropdown; the site policy-override modal is replaced by an inline panel; a shared datetime formatter is adopted across list views.</li>
<li>Verified: <span class="mono">go test ./...</span>, <span class="mono">buf lint</span>, <span class="mono">pnpm --filter @vben/web-antd typecheck</span>, <span class="mono">python3 scripts/i18n_check.py</span>, and signed linux/amd64 + linux/arm64 release packaging.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.0.7</span>
<span class="tl-date">2026-07-08</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Scoped IP control + site security overlays</div>
<div class="tl-body">
<p>IP-list controls now match how operators actually apply allow/deny decisions: lists are reusable data objects, and the effective action lives on the binding at global, policy, or site scope.</p>
<ul>
<li><strong>Global / policy / site scope.</strong> IP-list bindings now carry the action, bypass mode, and scope. The compiler enforces the visible priority order <span class="mono">global allow > global deny > site allow > site deny > site policy effective config</span>, including allow-list bypass modes for all-WAF or IP-list-only skip.</li>
<li><strong>IP Control UI.</strong> The IP Lists page is now an IP Control workspace with Global lists, an IP list library, reference counts, apply-now targets, and a scoped Lookup IP table that explains why a source address was allowed, denied, monitored, or left alone.</li>
<li><strong>Site Security protection.</strong> Site rows now open an effective-config drawer with inherited policy rows plus editable site deltas for CRS scalar overrides, rule overrides, custom rules, IP lists, rate limits, and diff/restore-inheritance controls.</li>
<li><strong>Security-event actions.</strong> Event drawers can quick allow/block a source IP at global or current-site scope with optional expiry, without navigating away from the investigation.</li>
<li><strong>Install ownership hardening.</strong> <span class="mono">tiyi install --now</span> now recurses through the state directory so root-owned embedded CRS files left by a prior <span class="mono">sudo tiyi standalone</span> run cannot break systemd applies.</li>
<li>Verified: <span class="mono">go test ./...</span>, <span class="mono">buf lint</span>, <span class="mono">pnpm --filter @vben/web-antd typecheck</span>, <span class="mono">python3 scripts/i18n_check.py</span>, and signed linux/amd64 + linux/arm64 release packaging.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.0.6</span>
<span class="tl-date">2026-07-06</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Alert-rule authoring redesign + dashboard/telemetry rework</div>
<div class="tl-body">
<p>The alert-rule builder becomes template-first, the dashboard and telemetry explorer get a ground-up rework, and a one-click bridge closes the "incident fired but I can't set an alert" gap.</p>
<ul>
<li><strong>Template-first alert rules.</strong> The builder now opens with a gallery of 8 vetted starters (critical incident, sustained 5xx surge, attack storm, agent offline, cert expiring, ACME failed, origin bypass, 4xx scan spike). The kind dropdown is grouped into Symptoms / Attack signatures / Infrastructure and exposes all 9 backend kinds — block-rate spike is no longer hidden. Each kind gets a structured form with anti-flap hold-before-firing / hold-after-resolve defaults.</li>
<li><strong>Incident → alert bridge.</strong> A firing incident now has "Create alert rule" and "Alert on this source IP" actions right in its drawer; they carry the incident into a reviewed draft (sessionStorage handoff) that dedups against existing rules and shows an origin banner.</li>
<li><strong>New default alert seeds.</strong> Every deployment now ships with a "Sustained 5xx surge" alert and an "Agent offline >10m" alert enabled on boot, so availability and data-plane liveness are covered out of the box.</li>
<li><strong>Dead-field fixes.</strong> <span class="mono">AttackClass</span> and source-IP CIDRs now actually filter security-threshold counts; the <span class="mono">MinSeverity=0</span> incident bug (which silently matched every severity) is fixed with an optional severity pointer; the dead <span class="mono">PolicyID</span> field is removed.</li>
<li><strong>Dashboard overview rework.</strong> A site filter and time-range control now scope the overview, reorganized into traffic, security-response, attack-distribution, top-K drill-down, and certificate rows.</li>
<li><strong>Telemetry explorer.</strong> A new QPS/blocked line chart with crosshair and tooltip; realtime queries now anchor to the newest flushed 10s bucket instead of wall clock, so the freshest durable point is always shown.</li>
<li><strong>Site TLS, agent groups, silences.</strong> Site TLS gains an explicit HTTP-only / redirect / serve-both / ACME-only mode control; agent-group selection adds explicit-agent + group pickers with a live match preview; alert silences gain value-picker matchers including attack class.</li>
<li>Verified: <span class="mono">go test ./...</span>, cross-build for linux/amd64 + linux/arm64, and license/release-key tests.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.0.5</span>
<span class="tl-date">2026-07-04</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Agent metrics trend + apply/update hardening</div>
<div class="tl-body">
<p>Operator-facing reliability for agent monitoring, site apply, and self-update.</p>
<ul>
<li><strong>7-day agent metric history.</strong> The agent detail drawer now retains seven days of per-agent samples and renders a trend view (requests/sec, blocked/sec, connections, CPU, memory), so operators see history instead of only the latest push.</li>
<li><strong>Dashboard drill-down time range.</strong> Clicking a top attacker on the dashboard now opens Security Events scoped to the dashboard's selected time range, instead of being locked to the last 15 minutes.</li>
<li><strong>IP-host site apply.</strong> Sites with an IP-literal primary host no longer fail their local HTTPS health check: Tiyi emits a single-uploaded-certificate fallback policy so Caddy can validate the certificate through the loopback path before applying.</li>
<li><strong>Startup preflight.</strong> <span class="mono">standalone</span> and <span class="mono">agent</span> now run a preflight before opening SQLite, generating the JWT warning, or starting Caddy; if the state directory, admin sockets, API listener, or proxy ports cannot be created or bound, the command exits with one actionable error (sudo, <span class="mono">tiyi install --now</span>, or writable <span class="mono">--state-db</span> / socket / high-port flags).</li>
<li><strong><span class="mono">tiyi update</span> precheck + progress.</strong> Self-update now shows download progress and pre-checks write privileges to the install directory, failing fast with a sudo/root hint instead of after a long download.</li>
<li>Verified: <span class="mono">go test ./...</span>, <span class="mono">buf lint</span>, <span class="mono">make build-all</span>, and license/release-key tests.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.0.4</span>
<span class="tl-date">2026-07-02</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Install diagnostics + tiyi doctor</div>
<div class="tl-body">
<p>Diagnostics polish for real Linux hosts: the copy-paste install path stays simple, while failures now point operators at the exact sudo PATH or port conflict to fix.</p>
<ul>
<li><strong>Default command stays simple.</strong> The public install command remains <span class="mono">curl -fsSL https://www.tiyisec.com/install.sh | bash && sudo tiyi install --now</span>; full-path guidance is shown only when sudo cannot resolve the installed binary.</li>
<li><strong>Installer environment checks.</strong> After installing or updating the binary, <span class="mono">install.sh</span> highlights sudo <span class="mono">secure_path</span> gaps and listeners on 80/443/8080, including stop/change-port guidance when another service owns a port.</li>
<li><strong><span class="mono">tiyi doctor</span>.</strong> A new non-mutating diagnostic command checks config loading, systemd availability, sudo PATH visibility, and role-specific port listeners for standalone, server, secondary, agent, and dashboard installs.</li>
<li><strong>CentOS/RHEL compatibility.</strong> Legacy OpenSSL builds that lack <span class="mono">pkeyutl -rawin</span> still get required SHA-256 verification in the installer; signed <span class="mono">tiyi update</span> remains the fully verified update path.</li>
<li>Verified: <span class="mono">go test ./...</span>, focused doctor unit tests, installer syntax + lockstep checks, temp-prefix installer smoke, and website/public-doc consistency checks.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.0.3</span>
<span class="tl-date">2026-07-02</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">China mirror install + signed update fallback</div>
<div class="tl-body">
<p>Operational polish for regions where GitHub is slow or unreliable, plus a tighter public documentation surface for runtime configuration.</p>
<ul>
<li><strong>GitHub primary, Gitee fallback.</strong> <span class="mono">install.sh</span> keeps GitHub as the default source and falls back to the Gitee release mirror when needed; operators can force the mirror with <span class="mono">TIYI_MIRROR=gitee</span>.</li>
<li><strong>Signed update mirror selection.</strong> <span class="mono">tiyi update</span> supports <span class="mono">auto</span>, <span class="mono">github</span>, and <span class="mono">gitee</span> mirror modes while preserving SHA-256 and Ed25519 verification before replacing the binary.</li>
<li><strong>User-oriented env docs.</strong> Runtime env documentation now follows <span class="mono">server.yaml</span> config keys only, recommends YAML for persistent service config, and documents config-derived names such as <span class="mono">TIYI_AUTH_BOOTSTRAP_ADMIN_PASSWORD</span> and <span class="mono">TIYI_LICENSE_KEY_PATH</span>.</li>
<li><strong>Public docs cleanup.</strong> Operator-facing docs and command help use <span class="mono">tiyi update</span>, remove private design references, and keep the website, GitHub public repo, and Gitee public repo aligned.</li>
<li>Verified: <span class="mono">go test ./...</span>, <span class="mono">buf lint</span>, installer syntax checks, and website/public installer lockstep.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.0.2</span>
<span class="tl-date">2026-06-28</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Frictionless install + deterministic first-run admin</div>
<div class="tl-body">
<p>A smoother five-minute path from install to a working dashboard login, with no journal-scraping for the admin password.</p>
<ul>
<li><strong>Deterministic first-run admin.</strong> <span class="mono">sudo tiyi install --now</span> (standalone) pre-creates the <span class="mono">admin</span> account before starting the service and prints the login — URL, username, and password — directly from the installer. The one-time secret never reaches journald, and the service skips its own random-password banner.</li>
<li><strong>Re-run = in-place update.</strong> When tiyi is already installed, <span class="mono">install.sh</span> uses the signed <span class="mono">tiyi update</span> path (downloading only when a newer release exists) and then re-installs + restarts the service, instead of pulling the full tarball again.</li>
<li><strong>Reachable by default.</strong> The API and dashboard now bind <span class="mono">0.0.0.0:8080</span> by default (still login-protected).</li>
<li>Verified: <span class="mono">go test ./...</span> and frontend unit tests green, plus a live install QA (credential banner, <span class="mono">0.0.0.0:8080</span> listener, working login) on a clean host.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.0.1</span>
<span class="tl-date">2026-06-27</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Multi-node cluster install + agent operations</div>
<div class="tl-body">
<p>Operational hardening for running Tiyi as a multi-node HA cluster, with every node managed by systemd on the default ports.</p>
<ul>
<li><strong>Cluster as systemd services.</strong> <span class="mono">tiyi install --mode server|secondary|agent</span> writes a hardened unit per node (80/443 via <span class="mono">CAP_NET_BIND_SERVICE</span>); the agent/secondary read their control-plane URL and one-use enrollment token from the unit's optional <span class="mono">EnvironmentFile</span> at <span class="mono">/etc/tiyi/tiyi.env</span> — the nginx-style drop-in. The agent-mode unit no longer carries control-plane-only flags it would reject.</li>
<li><strong>WAF on remote agents.</strong> Agents materialize the embedded CRS locally and rewrite a delivered bundle's CRS include paths to their own copy, so WAF-enabled sites deploy and block (SQLi / XSS / LFI → <span class="mono">403</span>) on remote agents, not just the control plane.</li>
<li><strong>Accurate agent metrics.</strong> Per-agent requests/sec, blocked/sec, and active connections now come from the data-plane counters and stream to the control plane (previously always zero); CPU, memory, bandwidth, and goroutines stay exact.</li>
<li><strong>Stable agent status.</strong> A connected, healthy agent stays online and no longer flaps online↔error — a metrics push refreshes liveness without masking a real apply error.</li>
<li><strong>Console fixes.</strong> Agent commands from the node list/detail no longer fail with "command kind is required"; the Install Agent code blocks are readable in dark mode and now offer a systemd-service variant; the dashboard shell is served <span class="mono">no-cache</span> so UI upgrades load without a manual hard refresh.</li>
<li>Verified: <span class="mono">go test ./...</span> green, plus a live three-node cluster QA (primary + secondary + agent) covering enrollment, the license scale gate, WAF blocking, metric sync, and agent commands.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.0.0</span>
<span class="tl-date">2026-06-26</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">First public release</div>
<div class="tl-body">
<p>Tiyi v3.0.0 is the first published GitHub Release: one signed binary distribution channel for Linux amd64 and arm64, with the installer served from both <span class="mono">www.tiyisec.com/install.sh</span> and the public GitHub raw URL.</p>
<ul>
<li>Release assets include per-platform tarballs, <span class="mono">SHA256SUMS</span>, <span class="mono">SHA256SUMS.sig</span>, and <span class="mono">release-manifest.json</span>.</li>
<li>Downloads verify against <span class="mono">release-key.pub</span> using SHA-256 plus Ed25519 signatures.</li>
<li>Operators can update an installed binary with <span class="mono">tiyi update --yes</span>.</li>
<li>GitHub release: <a href="https://github.com/zzmzm/tiyi/releases/tag/v3.0.0">Tiyi v3.0.0</a>.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-06-24</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">One-binary scale licensing + first-run admin</div>
<div class="tl-body">
<p>One binary whose single enforced distinction is <strong>scale, not features</strong>. Every WAF capability stays in every edition; a vendor-signed license changes only the remote-node budget and displayed plan.</p>
<ul>
<li><strong>Community = local standalone WAF, free forever.</strong> With no valid license the full product runs on the local node and permits zero remote agents. Importing a license activates its Pro or Enterprise node budget without replacing the binary.</li>
<li><strong>One always-on gate, inside the enrollment transaction.</strong> Legacy <span class="mono">license.mode</span> rows are ignored and writes are rejected. Count and insert share one transaction, so concurrent enrollments cannot slip past the signed budget. Existing sites, known agents, and local administration are <strong>never</strong> affected by license state.</li>
<li><strong>One trust anchor.</strong> Licenses are Ed25519-signed against the vendor public key embedded in the binary; public-key overrides are removed. The release target pins its SHA-256 fingerprint, and schema-versioned claims reject unknown plans, invalid budgets, and bad timestamp ordering.</li>
<li><strong>Safe degradation.</strong> Missing, invalid, tampered, or expired licenses resolve to Community. The data plane never stops, and known fingerprints always reconnect.</li>
<li><strong>Authenticated live status.</strong> System → About shows edition, licensee/expiry, and the live "N of M remote nodes" count. License metadata is no longer exposed by the public Version RPC.</li>
<li><strong>First-run admin.</strong> A standalone first boot with no users and no supplied credentials now auto-creates an <span class="mono">admin</span> with a random one-time password (printed once, stored only as a hash); <span class="mono">tiyi server</span> prints an actionable warning instead of leaving a silent dead-end login.</li>
<li>The vendor tool now creates private keys as <span class="mono">0600</span> with exclusive no-overwrite semantics; date-only expiries are inclusive through <span class="mono">23:59:59Z</span>.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-06-18</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Site path routing (M9)</div>
<div class="tl-body">
<p>A site used to match on host only. It can now fan out by <strong>path prefix</strong> to multiple upstream pools behind one host and one certificate — API-gateway-style routing — with the data plane and every cross-layer path scope kept consistent.</p>
<ul>
<li><strong>Translator fan-out.</strong> One host route expands into per-prefix subroutes, each to its own upstream pool; the existing zero-route output stays byte-identical to before — a golden test pins it.</li>
<li><strong>Per-route health.</strong> Each route gets a post-apply health probe (200–399 only); a down route backend fails the apply gate even when the site default is healthy.</li>
<li><strong>Unmatched policy.</strong> Unmatched paths fall through to the site default upstream, or to a strict <span class="mono">404</span> allowlist that needs no default.</li>
<li><strong>Full attribution.</strong> Every access-log row records the <span class="mono">route_id</span> and <span class="mono">upstream_id</span> that served it — through the parser, daily partitions, API/CLI filters, export, and the web log views.</li>
<li><strong>One canonical path scope.</strong> <span class="mono">/admin</span> vs <span class="mono">/Admin</span> resolve identically across the WAF, IP lists, and the rate limiter; a case-insensitive hint, live canonical echo, and named case-duplicate error guard every path input on web + CLI.</li>
<li>Drive it from <span class="mono">tiyi site routing get/set</span> or the new web routes editor. Post-implementation review hardened zero-route/404 validation, atomic compensation capture, stable route IDs across replace, strict per-pool endpoint/scheme/health-URI checks, and canonical scoped CRS overrides.</li>
<li>Verified: <span class="mono">go test ./...</span> green, <span class="mono">buf lint</span> clean, <span class="mono">vue-tsc</span> clean. Interactive browser QA was skipped at operator request.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-06-18</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Policy rate-limit editor</div>
<div class="tl-body">
<p>The policy <span class="mono">Rate Limits</span> tab graduated from a sketch into an operator-usable editor, with validation that refuses any rule the agent could not actually enforce.</p>
<ul>
<li><strong>Endpoint rows.</strong> Path-pattern limits (e.g. <span class="mono">/login</span>, <span class="mono">/api/auth/**</span>) keyed by the resolved client IP, returning HTTP <span class="mono">429</span> when blocking — or a <span class="mono">log-only</span> canary mode to observe a new limit before enforcing.</li>
<li><strong>Client scopes.</strong> Global ceilings across all paths keyed by <span class="mono">ip</span>, the <span class="mono">session</span> cookie, or a named header (<span class="mono">header:<name></span>). Burst <span class="mono">0</span> means the burst equals the per-minute limit.</li>
<li><strong>Inline validation.</strong> Save is blocked for missing paths/headers, non-positive limits, negative bursts, duplicate patterns/scopes, and unknown actions.</li>
<li><strong>Canonicalization.</strong> The API folds legacy <span class="mono">client_ip</span> → <span class="mono">ip</span> and <span class="mono">detection</span> → <span class="mono">log</span>; the compiler and runtime stay backward-compatible with historical rows. Enforced before Coraza, with no persistent collections.</li>
<li>Verified: <span class="mono">go test ./...</span> green, <span class="mono">buf lint</span> clean, <span class="mono">vue-tsc</span> clean.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-06-09</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">AI advisory layer + interactive Copilot</div>
<div class="tl-body">
<p>An optional, <strong>default-off</strong> LLM advisory layer that sits beside the deterministic WAF — in the control plane, never in the request path, and never mutating state on its own. It enriches incidents, drafts human-approved tuning, and answers plain-English questions about your logs.</p>
<ul>
<li><strong>Streamed analysis.</strong> A global <span class="mono">AI Copilot</span> slide-over explains any incident or single log event, or analyzes a filtered log result set — streamed token-by-token. Subjects are reduced to redacted whitelist projections, never raw audit JSON.</li>
<li><strong>Conversational agent.</strong> A <span class="mono">StreamChat</span> assistant answers open questions ("top 10 attackers in the last hour", "was example.com attacked today, from which IPs?") by tool-calling over five <em>read-only</em>, tenant-scoped log queries — it adds no new log surface.</li>
<li><strong>Advisory, never autonomous.</strong> Incident enrichment, policy-tuning patches, NL→log-query translation, draft custom rules (forced log-only canary), and campaign correlation are all proposals; the only state-changing path is an explicit operator <em>Apply</em> routed through the existing audited mutation, gated by <span class="mono">ai:apply</span>.</li>
<li><strong>Provider-agnostic + safe.</strong> OpenAI-compatible (OpenAI / Ollama / vLLM / LM Studio / llama.cpp) and Azure OpenAI; the key is KEK-encrypted, never a setting. Every prompt is redacted, dual-RBAC gated (<span class="mono">ai:read</span> plus the subject's own <span class="mono">incident:read</span>/<span class="mono">log:read</span>), rate-limited, and panic-isolated.</li>
<li>Verified: <span class="mono">go test ./...</span> green, <span class="mono">buf lint</span> clean, <span class="mono">vue-tsc</span> clean, <span class="mono">i18n_check.py</span> 0 missing, plus a Playwright QA pass against a mock provider.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-06-09</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Air-gap icons, telemetry explorer, static binary</div>
<div class="tl-body">
<ul>
<li><strong>Offline icon bundle.</strong> The admin console used to fetch icon SVGs from the public Iconify API at runtime — broken in air-gapped deployments. Icons are now collected from the frontend source + backend menu seeds and bundled at build time, registered before first paint.</li>
<li><strong>Telemetry explorer.</strong> A new Top-N / time-series view over the built-in telemetry pipeline (QPS, top-K, series) — no external time-series database required.</li>
<li><strong>Static binary.</strong> Release builds are now statically linked (<span class="mono">CGO_ENABLED=0</span>, pure-Go SQLite), dropping the build-host glibc dependency so the same artifact runs on older distributions (CentOS 7+).</li>
<li><strong>Operator fixes.</strong> Stable per-event access-log IDs for exact lookup, boot-time config apply, and a fix that lands permission-restricted operators on the first page they can actually open instead of a hard-coded dashboard.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-05-31</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Security incident aggregation — Phase 1–4</div>
<div class="tl-body">
<p>A <span class="mono">security_incident</span> layer on top of the per-request <span class="mono">security_event</span> stream. It composes with — does not replace — the source-of-truth event rows, the audit chain, the alert evaluator, and SIEM egress.</p>
<ul>
<li><strong>Phase 1 — aggregation.</strong> Same <span class="mono">(tenant, site, client_ip, attack_class)</span> over a sliding idle window collapses into one incident. A <span class="mono">security_incident_event</span> link table (with <span class="mono">event_id</span> uniqueness) holds the relationship; <span class="mono">security_event.incident_id</span> is a query-accelerator cache. Detection follows OWASP AppSensor; the wire shape follows the OCSF class taxonomy. Migrations <span class="mono">0020</span>–<span class="mono">0021</span>.</li>
<li><strong>Phase 2 — operator polish.</strong> Reopen, merge, live-tail, and per-site incident overrides. The UI keeps incidents in view after merge/reopen and closes the drawer on re-activate.</li>
<li><strong>Phase 3 — deterministic enrichment.</strong> Every incident is tagged at creation with its MITRE ATT&CK technique + sub-techniques and kill-chain stage (derived from <span class="mono">attack_class</span> + rule-id families) and a most-frequent country/ASN geo rollup. An operator-extensible <span class="mono">mitre_mapping</span> overlay (migration <span class="mono">0023</span>) overrides the built-in mapping. Example: an RCE + 933 event yields <span class="mono">T1190 [T1059, T1505.003]</span> / execution and geo CN / AS4134.</li>
<li><strong>Phase 4 — automated response.</strong> An <span class="mono">incident_response_rule</span> resource + <span class="mono">IncidentResponseRuleService</span> CRUD, and a responder that mutates live controls (deny IP list / rate-limit endpoint / alert webhook) on incident state transitions through the existing audited mutation paths. <strong>Default-off at three layers</strong> — no rule until created, every rule ships <span class="mono">enabled=0</span>, and the tenant kill switch <span class="mono">incident.response_enabled</span> defaults false. Every mutation is attributed to <span class="mono">system:incident-response</span> in the audit chain. A 60-second TTL sweeper auto-reverts expired deny entries through the audited <span class="mono">RemoveIPListEntries</span> path and recompiles the affected policies.</li>
<li>Surfaced via the <span class="mono">tiyi incident</span> / <span class="mono">tiyi incident-response</span> CLI verbs and a web UI under WAF → Automated Response. The Sigma-style operator-defined correlation DSL is explicitly deferred — the fixed 12-class + 4-axis-key correlation is the shipped v1.</li>
<li>Verified: <span class="mono">go test ./...</span> green, <span class="mono">buf lint</span> clean, <span class="mono">vue-tsc</span> clean, <span class="mono">i18n_check.py</span> 0 missing, plus a per-phase CLI/API runtime smoke test.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-05-31</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Alerting redesign — Phases 1–4 (D1–D14)</div>
<div class="tl-body">
<p>The evaluator's deterministic, poll-based shape and SQLite storage are kept; the missing half of every mature alerting system is added — the evaluation/notification split the industry converged on (Prometheus + Alertmanager), with no new external dependencies.</p>
<ul>
<li><strong>Correctness fixes.</strong> Closed a UI-exposed rule kind the backend never implemented, threshold counts that were silently capped at the query page size, and channel kinds whose UI/API contracts did not match their delivery implementation.</li>
<li><strong>Lifecycle, not fire-once.</strong> A notification dispatcher drives <span class="mono">pending → firing → recovered/resolved</span> with <span class="mono">for</span>-duration debouncing, re-notification while an alert stays open, and auto-resolve — replacing the old "page exactly once, then go quiet" behavior.</li>
<li><strong>Durability.</strong> A durable notification outbox (migration <span class="mono">0026</span>) plus retry-with-backoff closes the crash-window that could permanently drop the only notification for a new alert. Evaluator state persists in <span class="mono">alert_eval_state</span> (migration <span class="mono">0025</span>).</li>
<li><strong>Security.</strong> Channel secrets (webhook URLs, PagerDuty keys, Slack tokens) moved from plaintext JSON to KEK-encrypted storage and are no longer returned verbatim on read or into the audit chain (migration <span class="mono">0024</span>).</li>
<li><strong>Quiet by default.</strong> Grouping, inhibition, and silences, plus a per-alert note timeline, rule lifecycle, and channel routing.</li>
<li>Implemented the previously-missing RPCs, audited silence mutations, and pruned dead cert UI along the way.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-05-31</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Pre-merge security + QA hardening</div>
<div class="tl-body">
<ul>
<li><strong>Fail-open RBAC gap closed</strong> — several system and certificate admin RPCs did not enforce a permission check and defaulted to allowing the call. Now gated, with a <span class="mono">rbac_coverage_test.go</span> regression test that asserts coverage across the surface.</li>
<li><strong>Standalone dashboards read zero traffic</strong> — <span class="mono">telemetry.enabled</span> defaults true, which gates off the legacy rollup writer, but the embedded <span class="mono">StoreAccess</span> path never called <span class="mono">telemetry.Ingest</span>, so access traffic was recorded nowhere the dashboard reads. Fixed by ingesting in <span class="mono">StoreAccess</span>, with a regression test.</li>
<li><strong>Race-aware test budget</strong> — <span class="mono">TestFlushReturnsTrueWhenDrained</span> had a hard 2s drain budget the race detector's overhead blew past; made it race-aware. Also fixed a pre-existing <span class="mono">gofmt</span> drift so <span class="mono">make lint</span> is green.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">v3.0.0-rc.1</span>
<span class="tl-date">2026-05-27</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Direct page-jump pagination for log views</div>
<div class="tl-body">
<p><span class="mono">CursorRequest.offset</span> with a 100k cap, mutually exclusive with the cursor path. Offset accounting runs over post-filter rows so CIDR/rule filters skip the right number of <em>matched</em> rows. The frontend drops <span class="mono">cursorStack</span> + <span class="mono">walkForwardTo</span> in favor of computing offset directly from the target page.</p>
<ul>
<li>4 store tests + 2 wire tests pin offset semantics and rejection of cursor+offset misuse</li>
<li>Files: <span class="mono">proto/tiyi/v1/common.proto</span>, <span class="mono">internal/store/log_repo.go</span>, <span class="mono">internal/api/log_handler.go</span>, three log list <span class="mono">.vue</span> files</li>
<li>Verified: <span class="mono">go test ./...</span> green, <span class="mono">buf lint</span> clean, <span class="mono">vue-tsc</span> clean</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-05-26</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Six post-deploy QA fixes</div>
<div class="tl-body">
<ul>
<li><strong>DEF-002</strong> — IP-list <span class="mono">geo:CC</span> entries accepted at the API boundary (alpha-2/alpha-3 + curated <span class="mono">PRIVATE</span>/<span class="mono">LOOPBACK</span> buckets); the seclang compiler already expanded them at compile time, so the entry now flows through end-to-end without forking the dataset.</li>
<li><strong>DEF-003</strong> — bookmarkable parametric routes (<span class="mono">/agents/:id</span>, <span class="mono">/waf/policies/:id</span>) re-registered after the backend menu generation drops them; <span class="mono">router.hasRoute</span> guards against double-registration.</li>
<li><strong>DEF-004</strong> — <span class="mono">ResolveAgentGroup</span> now resolves selectors against <span class="mono">ListAgents</span> for either a saved-group id or an ad-hoc spec preview (matching by <span class="mono">match_agent_ids ∪ tag_selectors</span>).</li>
<li><strong>DEF-005</strong> — standalone agent commands now drained through a 2-second consumer; <span class="mono">reload</span> calls <span class="mono">ApplyActiveSites</span>; <span class="mono">restart</span> succeeds as a no-op (tearing down the local management plane is the wrong default).</li>
<li><strong>DEF-006</strong> — trust-profile mutations now write <span class="mono">trust.update_profile</span>, <span class="mono">trust.reset_profile</span>, <span class="mono">trust.update_site_override</span>, <span class="mono">trust.reset_site_override</span>, <span class="mono">trust.refresh_cdn</span> rows into the audit chain.</li>
<li><strong>DEF-007</strong> — access log default <span class="mono">RecordingMode</span> flipped from <span class="mono">"off"</span> to <span class="mono">"on"</span> at three layers, matching the System Settings copy "默认访问日志模式 = 完整(自动)".</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-05-24</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Security hardening + frontend cleanup</div>
<div class="tl-body">
<p>Code-review pass on the full backend tree closed every actionable finding without dropping a feature.</p>
<ul>
<li><strong>KEK persistence</strong> — server only calls <span class="mono">db.SetKEK(envelope.LoadKEK)</span> when <span class="mono">crypto.kek_file</span> is explicitly configured. Previous unconditional override generated an ephemeral KEK on every restart, silently breaking encrypted cert keys, ACME accounts, and DNS provider credentials.</li>
<li><strong>Persistent ed25519 bundle signing key</strong> — singleton row in <span class="mono">bundle_signing_key</span>, KEK-encrypted at rest. Pre-warmed in <span class="mono">store.Open</span> after migrations and Bootstrap. Replaces the per-restart in-memory generator that was defeating the agent's TOFU pin.</li>
<li><strong>Agent revision replay protection</strong> — <span class="mono">applyConfigUpdate</span> rejects any incoming bundle whose revision is not strictly greater than <span class="mono">LastAppliedRevision</span>.</li>
<li><strong>Migration drift detection</strong> — <span class="mono">internal/store/migrate.go</span> records SHA-256 of every applied migration; boot aborts on drift.</li>
<li><strong>Frontend cleanup</strong> — dropped 16 Vben demo pages and 5 supporting <span class="mono">.ts</span> files, rewrote the layout shell, fixed broken role checks comparing against <span class="mono">'standby'</span>/<span class="mono">'active'</span> (real values are <span class="mono">standalone|primary|secondary</span>).</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-05-24</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">WAF tune review — PRs 1–8</div>
<div class="tl-body">
<p>Deep review of the policy tune drawer surfaced and closed every actionable defect across eight coordinated PRs. Highlights:</p>
<ul>
<li><strong>Audit chain coverage</strong> — every Upsert/Update/Delete on <span class="mono">rule_override</span>, <span class="mono">custom_rule</span>, <span class="mono">ip_list</span>, <span class="mono">ip_list_binding</span>, <span class="mono">rate_limit_endpoint</span> now appends a hash-chained mutation row; previous code silently bypassed it.</li>
<li><strong>Score-override emit</strong> — switched from additive <span class="mono">setvar:tx.inbound_anomaly_score=+N</span> to per-bucket <span class="mono">tx.<sev>_anomaly_score_pl<pl></span> reset+set, eliminating double-count.</li>
<li><strong>Bulk upserts atomic</strong> — <span class="mono">BulkUpsertRuleOverrides</span> runs N upserts in a single transaction, all-or-nothing, with one bundle-hash refresh and one apply at the end.</li>
<li><strong>Rule override action enum</strong> — <span class="mono">DEFAULT|DISABLE|LOG_ONLY|SCORE_OVERRIDE</span>; legacy <span class="mono">disabled</span> bool and <span class="mono">action</span> string deprecated; <span class="mono">score_override</span> migrated to <span class="mono">optional int32</span>.</li>
<li><strong>Field-mask discipline</strong> — UpdatePolicy enforces that empty <span class="mono">ip_list_bindings</span> only wipes when <span class="mono">update_mask.paths</span> includes the field.</li>
<li><strong>Path-prefix validation</strong> — rejects non-printable ASCII and SecLang quoting metacharacters at the API boundary.</li>
<li><strong>i18n locale parity</strong> — 1786/1786 keys balanced across en-US and zh-CN with vue-i18n linked-message escapes for every <span class="mono">@</span>-prefixed literal.</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-05-16</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Client-IP trust pipeline</div>
<div class="tl-body">
<p>One trust pipeline ends per-component XFF parsing fragmentation. Replaces the blind XFF-first parse in the rate-limiter — the security-class fix.</p>
<ul>
<li><span class="mono">tenant_settings.trust_profile_json</span> + <span class="mono">site.trust_profile_override_json</span></li>
<li>Auto-fetched CIDR snapshots for Cloudflare, Fastly, Akamai, CloudFront, Front Door, GCLB; refresher with per-job goroutine, jittered interval, and panic isolation</li>
<li>Translator emits <span class="mono">trusted_proxies</span> + <span class="mono">trusted_proxies_strict</span> per <span class="mono">http.servers</span> block</li>
<li>Strict right-to-left walk with verbatim Caddy v2 byte-for-byte semantics</li>
<li><span class="mono">TrustService.Explain</span> traces every header to the resolved client IP for any (peer, headers) tuple</li>
<li>Default-on alert: <span class="mono">Origin Bypass Attempt</span> (5-minute window, severity high)</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-05-10</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Telemetry pipeline (Phase 1–6)</div>
<div class="tl-body">
<p>Full counters → samples → API tree → read API → Prometheus exporter pipeline end-to-end.</p>
<ul>
<li><strong>Phase 1</strong> — sharded ingress ring, 10s open-bucket aggregator, per-day SQLite partitions at <span class="mono">logs/rollup_10s/YYYY-MM-DD.db</span></li>
<li><strong>Phase 2</strong> — Misra-Gries Top-K with <span class="mono">__other__</span> bucket so SUM(*) equals true traffic; per-day dictionary log; hour/day/month downsamplers</li>
<li><strong>Phase 3</strong> — per-IP LRU + global sample rings; append-only circular-file WAL with CRC; replay-on-boot</li>
<li><strong>Phase 4</strong> — per-site URL prefix tree with <span class="mono">MaxNodesPerSite</span> + <span class="mono">MaxDepth</span>; janitor evicts idle low-traffic leaves into <span class="mono">api_tree_archive</span></li>
<li><strong>Phase 5</strong> — REST API at <span class="mono">/api/v1/telemetry/*</span> + frontend explorer</li>
<li><strong>Phase 6</strong> — Prometheus exporter at <span class="mono">/metrics</span> on the local admin socket</li>
<li><strong>Legacy cutover</strong> — <span class="mono">logsink.recordAccessRollup</span> short-circuits when telemetry is active, ending the per-event <span class="mono">traffic_rollup_minute</span> write that amplified I/O at high event rates</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-05-09</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Frontend shell complete</div>
<div class="tl-body">
<p>Every server-seeded menu route resolves to a real page, the core admin pages are wired end-to-end, and the reusable Tiyi UI components ship.</p>
<ul>
<li>Dashboard with <span class="mono">GetDashboardStats</span>, <span class="mono">GetTrafficTimeseries</span>, <span class="mono">GetAttackDistribution</span>, <span class="mono">GetTopAttackers</span>, <span class="mono">StreamAlerts</span></li>
<li>Policy editor with 12 tabs, deep-linkable: CRS Core · HTTP Policy · Limits · Rule Tuning · IP Lists · Custom Rules · Plugins · Rate Limits · Exclusions · Preview · Versions · Test Lab</li>
<li>System branch: Health, Log Pipeline, Users, Roles, Settings (10 tabs), Replication, Updates (4 tabs), About</li>
<li>Alert Rules, Alert Channels, IP Lists, Agents detail, Telemetry Explorer, Audit + diff drawer</li>
<li><strong>Resilience primitives</strong>: <span class="mono">useResilientStream</span>, <span class="mono">formatRpcError</span>, <span class="mono">useResponsiveWidth</span></li>
<li>Full zh-CN + en-US, all <span class="mono">$t()</span> call sites resolve</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-05-07</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">WAF policies — full lifecycle</div>
<div class="tl-body">
<p>Store/API/Web/CLI CRUD; built-in templates (Strict/Standard/Permissive); version snapshots and rollback; engine-state switch; CRS binding; rule-override / custom-rule / IP-list / plugin / rate-limit tuning paths; SecLang preview; CRS impact preview; policy test lab.</p>
<ul>
<li>Deterministic SecLang compilation with CRS best-practice advisory warnings</li>
<li>Per-site policy overlays cover scalars plus site-scoped rule/custom/IP-list/rate-limit tuning without forking the policy</li>
<li>CRS rule-exclusion packages (WordPress, Drupal, Nextcloud, phpBB, phpMyAdmin, XenForo, cPanel, DokuWiki) with offline archive upload</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-05-07</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Remote agents</div>
<div class="tl-body">
<p>One-use enrollment tokens, mounted <span class="mono">AgentStream</span>, <span class="mono">tiyi agent</span> runtime, live remote bundle delivery / cache / apply-result reporting, best-effort remote access/security/error log upload.</p>
<ul>
<li>Pinned bundle-signing public-key verification, TOFU on first contact</li>
<li>Periodic metrics push (memory/CPU)</li>
<li>Agent role tracking (primary / secondary / agent)</li>
<li><span class="mono">tiyi agents issue-token | install-script | send-command | commands</span> CLI</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-05-06</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">ACME — Phases A through D</div>
<div class="tl-body">
<ul>
<li><strong>Phase A</strong> — real RFC 8555 client (wrapping <span class="mono">golang.org/x/crypto/acme</span>) replaces the self-signed stub; per-(tenant, directory) account lifecycle</li>
<li><strong>Phase B</strong> — multi-agent HTTP-01 with a loopback responder on every agent and (token, key_authorization) broadcast through the agent stream</li>
<li><strong>Phase C</strong> — DNS-01 with pluggable driver registry: full Cloudflare driver, Route53 + Aliyun credential-validating stubs; authoritative-NS-aware propagation checker</li>
<li><strong>Phase D</strong> — observability: dashboard exposes <span class="mono">certs_expiring_14d</span>, <span class="mono">acme_renewals_failed_24h</span>, etc.; default alerts seeded</li>
<li>UI: "Issue (ACME)" modal, per-row Renew action, "DNS Providers" page</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-05-05</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">CRS catalog + sites + log forwarding</div>
<div class="tl-body">
<ul>
<li>OWASP CRS 4.25.0 ruleset embedded in the binary, auto-imported on first boot</li>
<li>GitHub-release fetch via <span class="mono">SystemService.ListUpstreamCrsReleases</span>; offline archive upload also supported</li>
<li>Sites lifecycle: store/API/Web/CLI CRUD/status; real Caddy JSON preview; uploaded cert and WAF policy selection</li>
<li>Standalone runtime: Caddy access logs through <span class="mono">tiyi.log_forwarder</span>; Coraza <span class="mono">SecAuditLogType tiyi</span>; live security tail</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-05-03</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">WAF interception fix + boundary <span class="mono">defer recover</span></div>
<div class="tl-body">
<p>Closed the "empty reply from server" bug on blocked requests. Root cause: a typed-nil-in-interface panic in <span class="mono">audit_writer.auditMessages</span> (<span class="mono">message.Data()</span> returned a non-nil interface wrapping a nil <span class="mono">*MessageData</span>) unwound coraza-caddy's deferred <span class="mono">tx.ProcessLogging</span> before the WAF middleware returned its <span class="mono">caddyhttp.HandlerError</span>.</p>
<p>Three boundaries now have <span class="mono">defer recover()</span> at function level: <span class="mono">audit_writer.Writer.Write</span>, <span class="mono">log_forwarder.writeAccessLine</span>/<span class="mono">writeWAFLine</span>, and <span class="mono">logsink</span> workers. Cross-layer canary at <span class="mono">/debug/logsink/stats</span> exposes a <span class="mono">panicked</span> counter.</p>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-05-01</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Alerts, audit SIEM, full standalone observability</div>
<div class="tl-body">
<ul>
<li>Alert evaluator with <span class="mono">security_threshold</span>, <span class="mono">error_threshold</span>, <span class="mono">audit_failure</span> kinds and webhook / Slack / PagerDuty / Feishu / WeCom delivery</li>
<li>Audit-chain rows forwarded through SIEM with explicit <span class="mono">siem.filter.include_audit</span> flag</li>
<li>RFC 5424 / CEF / LEEF formats over TCP / UDP / unixgram</li>
<li>Vben pages for security, access, error, live-tail, audit, alerts (active / rules / channels)</li>
</ul>
</div>
</div>
<div class="tl-item shipped">
<span class="tl-dot"></span>
<div class="tl-head">
<span class="tl-version">2026-04-28</span>
<span class="tl-tag shipped">Shipped</span>
</div>
<div class="tl-title">Foundation</div>
<div class="tl-body">
<ul>
<li>Go 1.25.0; ConnectRPC 1.19.2; 14 services under <span class="mono">proto/tiyi/v1/*.proto</span></li>
<li>SQLite schema (full Phase-1) — audit, CRS, policy_version, config_bundle</li>
<li>Server modes: <span class="mono">standalone</span>, <span class="mono">server</span>/<span class="mono">primary</span>, <span class="mono">secondary</span>/<span class="mono">standby</span>, <span class="mono">dashboard</span>, <span class="mono">agent</span></li>
<li>Local admin socket with OS-file-permission auth</li>
<li>Argon2id login, JWT issue/verify, refresh-token chain</li>
<li>Koanf config loader; SQLite open/migrate/bootstrap; permission and menu seeds</li>
<li>Audit spine — repo, hash chain, verifier, AuditService, mutation audit rows, <span class="mono">tiyi audit</span> CLI</li>
</ul>
</div>
</div>
</div>
<div style="text-align:center; margin: 56px 0 16px;">
<a class="btn btn-ghost" href="index.html">← Back to home</a>
</div>
</div>
</section>
<footer class="footer" style="margin-top: 0;">
<div class="container">
<div class="footer-bottom" style="padding-top: 0; border: 0;">
<span>© <span data-year>2026</span> Tiyi · proprietary distribution · Apache-2.0 upstreams retained</span>
<span class="build">build v3.5.4 · 2026-08-13</span>
</div>
</div>
</footer>
<script src="assets/js/main.js" defer></script>
</body>
</html>