From 31820b5c2382e128703774b36c93c8c5595413c8 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Sun, 13 Sep 2026 17:24:16 +0700 Subject: [PATCH] docs(audit): close SEC-08, SEC-09 and SEC-26, and record how they were verified Marks the three IDs Done and records what actually shipped, in the dependency order that mattered: SEC-26 first, because the other two key on a client address that was forgeable until it landed. SEC-26 (PR #13). ServerConfig gains trustedProxies and trustedPlatform, applied in NewServer before any middleware. An unparseable CIDR fails startup rather than quietly dropping the trust boundary. The default is the loopback, RFC1918, IPv6 unique-local and link-local ranges rather than Gin's trust-everything default, and trustedPlatform defaults to empty on purpose - an edge that appends instead of overwriting would hand the header back to the caller. SEC-09 (PR #13). isCredentialLocked now takes the client address and applies two windows: principal-and-address for one source grinding on one account, and address-across-all-principals for credential stuffing. The four existing lockout tests failed when the key changed, because they seeded credential logs without a client address - they encoded the vulnerable semantics. That failure is the evidence the fix is real. SEC-08 (PR #14). Six public routes limited, with the channel webhooks, websockets, HMAC webhooks and the whole dashboard deliberately exempt; a test fires 1000 requests at five of those routes and fails on any 429, because a platform that gets throttled on its webhook eventually disables the delivery and takes a channel offline. Also recorded, because leaving them out would make the document read better than the work was: - Retry-After shipped as int(seconds)+1 at first, which returns 61 for a 60 second window whenever the clock has not ticked between requests. My own test caught it. - The limiter could not be validated under -race. This repository builds with CGO disabled and go test -race requires cgo. The concurrency test still has teeth, since an unguarded concurrent map write panics rather than merely miscounting, but it is not a substitute for the race detector. - SEC-26, SEC-09 and SEC-08 have not been ported upstream. They cannot be replayed the way the upload fix was: config.go on dev carries fork-only fields, and server.go on dev already contains the storage hardening still sitting in the open upstream PR #40. - A second git incident. A file showed as modified in git status while git diff was empty - a stat-cache artifact after gofmt then restore - and it blocked a fast-forward. Resolved by proving the content identical with git diff --exit-code HEAD, then git add --renormalize, then confirming the staged diff was empty before continuing. Remaining count moves from 89 to 86. Every declared number in the document is cross-checked against the register tables by script across 18 sites plus the per-prefix stats table and its total row; the check passes. --- docs/CROVE_DESK_AUDIT.html | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/CROVE_DESK_AUDIT.html b/docs/CROVE_DESK_AUDIT.html index a07af88b..c6ecea24 100644 --- a/docs/CROVE_DESK_AUDIT.html +++ b/docs/CROVE_DESK_AUDIT.html @@ -111,7 +111,7 @@
main đã merge ngược vào dev, đã release v1.7.0-crove.1. Đọc mục này trước, register bên dưới giữ nguyên nội dung gốc để đối chiếu.
+ ✅ Round 5 — 2026-09-10 → 09-12: 24/115 ID đã đóng, 2 ID đóng một phần, 3 ID đã bác bỏ, 3 ID tôi tự cải chính. SEC-26 là ID mới của round này và đã đóng luôn. Upstream sync xong, main đã merge ngược vào dev, đã release v1.7.0-crove.1. Đọc mục này trước, register bên dưới giữ nguyên nội dung gốc để đối chiếu.
e66b1043 trên dev, 8 file +598/−24. Bốn lớp:
(1) storage.ValidateUpload chặn extension + payload là tài liệu browser thực thi được (HTML/SVG/XML/XSL/script/PHP-ASP-JSP) và sniff 512 byte đầu thay vì tin Content-Type client khai — đổi tên trang thành .png không qua được;
@@ -133,7 +133,7 @@ 2f369422, +282 chuỗi mỗi locale. Điểm hay nhất: sub-agent không dùng nguồn tôi chỉ (270d221d/31259378) mà tìm ra 72039b90 "feat: enhance localization for community features and documentation center" — nằm trên upstream/dev, không phải ancestor của nhánh này, chỉ sửa đúng 2 file messages. Tôi đã verify độc lập bằng script riêng: 439 key en-US và 440 key zh-CN khớp nguyên văn từng byte với commit đó, 0 key bịa; khác biệt cố ý duy nhất là supportPublic.nav.help ("Docs" thay vì "Help", khớp rename help→docs của nhánh này) và một chỗ viết hoa. vi-VN.json không tồn tại ở 72039b90 nên 282 chuỗi tiếng Việt là viết mới; tôi đếm lại thì 277/282 là tiếng Việt thật, 5 chuỗi còn lại đúng là thuật ngữ không dịch (Agent, Workflow, Email, Slug, URL). Số namespace sau fix: workflowRun 21→54 · supportPublic 138→234 · docWorkbench 0→70 · supportCommunityCategory 0→46 · supportConfig 0→31 · docs 0→5 · supportCommunityAdmin +1 — giống hệt nhau ở cả 3 locale. Tôi cũng xoá 2 duplicate key cùng lớp với BUG-01 (conversation.cancel ở en/zh, skillDefinition.status ở zh) sau khi xác nhận hai bản trùng giá trị y hệt nên bỏ bản đầu không đổi hành vi.supportConfig không có bản tương đương — content thiếu thật" → sai, nó tồn tại ở 72039b90 với 43 key, recover 31 và cố ý bỏ 12 key thuộc tính năng AI customer service chưa có trên nhánh này; (2) tôi đếm docWorkbench 58 reference → thực tế cần 70 key vì thiếu 12 key sinh động từ t(`docWorkbench.${status}ConfirmTitle`) với status ∈ {draft,published,hidden} (help-workbench.tsx:287-300). Bài học: kiểm kê key tĩnh luôn đếm thiếu ở codebase này — tôi đã tự verify cả 17 khai triển động (docWorkbench 12 + supportPublic.comment.sort 3 + supportPublic.help.previous/next 2) đều tồn tại ở cả 3 locale.pnpm/npx/node -v), nên tôi tự chạy lại toàn bộ: tsc --noEmit PASS · node --test 68/68 pass, 13 suite, 0 fail · eslint 6 error / 47 warning so baseline 7/48 của PROC-16, không error nào ở file đã sửa (giảm 1 là nhờ BUG-25 đã fix) · pnpm install --frozen-lockfile PASS · web/pnpm-lock.yaml hash khớp HEAD từng byte. Verifier i18n tôi viết riêng: ALL CHECKS PASSED — 0 duplicate key ở mọi độ sâu, 7 namespace khớp tuyệt đối giữa 3 locale, không value rỗng, placeholder giống nhau, và mọi t() trong 313 file source đều resolve; key duy nhất còn thiếu toàn cục là channel.processing (ngoài phạm vi, để nguyên).internal/email/client.go với 6 provider và Brevo đang được cấu hình thật trong .env.internal/email/client.go với 6 provider và Brevo đang được cấu hình thật trong .env) · SEC-10 session token plaintext · SEC-12 webhook replay opt-in · SEC-13 asset URL vĩnh viễn (cái này khó: outbound channel bắt buộc phải có URL công khai để Meta/Telegram tải media, nên thêm expiry là đổi contract) · ARCH-01 multi-tenancy (quyết định sản phẩm) · 135 chuỗi vi-VN vẫn là tiếng Anh.supportPublic.* có sẵn từ trước trong vi-VN.json vẫn là tiếng Anh (tôi đếm bằng script, không phải ước lượng). Key tồn tại nên không render raw key, nhưng khách Việt thấy chữ Anh trên chính trang support công khai của một sản phẩm Việt. Ngoài ra 4 namespace giờ thành mồ côi sau rename — supportHelpWorkbench (70 key), supportFaqCategory (46), help (5), supportQuestionAdmin (23) — nên dọn ở một commit riêng.BindEnv khiến PORT/DATABASE_URL đè lên AGENT_DESK_*" là sai. Chứng minh bằng hai cách độc lập: (1) test đặt hai biến xung đột giá trị (PORT=9999 + AGENT_DESK_SERVER_PORT=8090) rồi cố tình đảo thứ tự về kiểu cũ → vẫn ra 8090; (2) source viper@v1.21.0 viper.go:1227-1245 — khi automaticEnvApplied thì viper return ngay sau getEnv(mergeWithEnvPrefix(...)), trước khi đọc v.env[lcaseKey]. Vậy commit reorder 51 dòng ở 0c6b9061 là no-op, và nó đã lên cả upstream/main qua PR #39 kèm một comment mô tả sai cơ chế. Tôi giữ reorder (bỏ sẽ lệch upstream, tạo conflict về sau) nhưng sửa comment, và thêm 2 test precedence — test cũ đặt hai spelling cùng giá trị nên không thể phát hiện đảo thứ tự. Vế phụ của SEC-14 là thật và đã fix: DSN sniffer đổi engine mà không log (910bd4f0).SetTrustedProxies/TrustedPlatform/ForwardedByClientIP ở đâu trong repo (grep: 0 kết quả) nên Gin dùng defaultTrustedCIDRs = [0.0.0.0/0, ::/0]; validateHeader (gin@v1.12.0 gin.go:482-501) quét X-Forwarded-For phải→trái và chỉ dừng ở proxy không tin cậy, mà mọi proxy đều tin cậy → nó chạy tới i == 0 và trả về phần tử đầu tiên do client tự đặt. Hệ quả: t_login_credential_log.client_ip và t_user.last_login_ip đang lưu IP bịa, và bất kỳ rate limit hay lockout nào khoá theo IP đều bị qua mặt bằng một header. Vì SEC-08 và SEC-09 đều định khoá theo IP, sửa chúng trước khi sửa SEC-26 sẽ tạo ra bảo mật giả. Fix có sẵn trong gin: gin.go:85 định nghĩa PlatformCloudflare = "CF-Connecting-IP" — header Cloudflare ghi đè chứ không nối thêm, khớp đúng deployment sau Cloudflare Tunnel.| ID | Sev | Vấn đề | Vị trí | Ver |
|---|---|---|---|---|
| SEC-07 Done | High | WebSocket bypass permission conversation.view. REST gate ở 5 endpoint; WS trả true cho mọi admin-role session. Payload chứa full nội dung tin nhắn. ID tuần tự → enumerate được. | services/ws_service.go:624-628, 301-317 · vs handlers/dashboard/conversation_handler.go:24,75,108,127,287 | ✓ |
| SEC-08 | Med | Không có rate limiting ở bất kỳ đâu (grep 0 kết quả). Khuếch đại SEC-01 (flood 20MB), SEC-09, spam register, và DoS tốn tiền LLM qua flood tin nhắn. | toàn bộ internal/ | ✓ |
| SEC-09 | Med | Credential lockout key theo username, không theo IP → attacker biết username (vd admin) khóa tài khoản thật 15 phút, lặp vô hạn = DoS vĩnh viễn. | services/auth_service.go:465-479 | ✓ |
| SEC-08 Done | Med | Không có rate limiting ở bất kỳ đâu (grep 0 kết quả). Khuếch đại SEC-01 (flood 20MB), SEC-09, spam register, và DoS tốn tiền LLM qua flood tin nhắn. Đã đóng (PR #14, 2026-09-13). Package internal/pkg/ratelimit fixed-window in-process + middleware, gắn vào 6 route public: login 20 · register 10 · session_exchange 120 (phải chịu được cả văn phòng sau một NAT) · upload attachment+image 30 dùng chung · doc feedback 20; window 60s, bật/tắt và window đều config được. Trả 429 thật + Retry-After (làm tròn lên) + body JsonResult — đã đọc web/lib/api/client.ts:54-66 để xác nhận frontend parse body trước rồi mới check response.ok, nên message đã dịch vẫn hiện, và im.ts dùng chung request đó. Cố ý KHÔNG limit /api/third/* (platform nhận 429 sẽ ngừng retry rồi tắt luôn webhook = mất cả kênh), /api/ws/*, /api/webhooks/* (đã HMAC), /api/dashboard/* (đã auth, staff chung IP sẽ khoá lẫn nhau) — và có test bắn 1000 request vào 5 route đó, fail nếu ra bất kỳ 429 nào. 15 test mới, gồm test 16 goroutine × 3200 call đòi đúng 1000 được phép. Giới hạn: counter per-process nên chạy nhiều replica thì bound yếu đi theo số replica; và không chạy được -race vì repo build với CGO tắt. | toàn bộ internal/ | ✓ |
| SEC-09 Done | Med | Credential lockout key theo username, không theo IP → attacker biết username (vd admin) khóa tài khoản thật 15 phút, lặp vô hạn = DoS vĩnh viễn.Đã đóng (PR #13, 2026-09-13). isCredentialLocked giờ nhận thêm clientIP và áp hai cửa sổ: (principal, IP) cho brute-force một tài khoản từ một nguồn, và (IP trên mọi principal) cho credential stuffing — auth.maxFailedAttemptsPerIP, mặc định 4× maxFailedAttempts, tắt maxFailedAttempts là tắt cả hai. IP không xác định được chuẩn hoá về một bucket unknown và loại khỏi cửa sổ theo IP, để thiếu IP không gộp mọi caller vào một lockout. createLoginCredentialLog cũng chuẩn hoá lúc ghi để hai bên đọc/ghi không lệch nhau. Bằng chứng fix có thật: 4 test lockout cũ FAIL khi tôi đổi — chúng seed log không có ClientIP, tức đang mã hoá đúng cái semantics có lỗ hổng. Đã sửa chúng và thêm 2 test mới (IP tấn công bị khoá trong khi chủ thật từ IP khác vẫn đăng nhập được; spray 4 username từ 1 IP bị cửa sổ theo IP bắt). | services/auth_service.go:465-479 | ✓ |
| SEC-10 | Med | Session token lưu plaintext trong DB + localStorage. Entropy tốt (192-bit crypto/rand) nhưng đọc được DB hoặc chạy được JS cùng origin (SEC-01) là lấy credential sống. | services/auth_service.go:281,289 | ✓ |
| SEC-11 Done | Med | Threads webhook verify bypass 2 đường: (a) route trần /api/third/threads/webhook echo hub.challenge không xác minh; (b) bỏ header X-Hub-Signature-256 là qua. LINE/Viber trong cùng commit verify vô điều kiện. | handlers/third/threads_handler.go:15-42 · services/threads_inbound_service.go:45 | ✓ |
| SEC-12 | Med | Webhook replay protection là opt-in: nhánh t=,v1= có window 5 phút, nhưng nhánh fallback sha256= không timestamp/nonce. Nhánh 1 fail còn fallthrough sang nhánh 2. Attacker tự chọn bỏ timestamp. | services/webhook_sync_service.go:56-107 | ✓ |
| SEC-13 | Med | Asset local: URL công khai vĩnh viễn, GetSignedURL = URL trần không chữ ký không expiry (OSS thì có 600s). URL nằm trong access log (requestLogMiddleware log path) và bị đẩy sang kênh thứ ba. Attachment customer thường chứa PII. | services/storage/local.go:53-55 · bootstrap/server.go (StaticFS + requestLogMiddleware) | ✓ |
| SEC-14 Corrected Done | Med | BindEnv đặt tên legacy không prefix đứng đầu → DATABASE_URL/PORT trôi nổi override cả YAML lẫn AGENT_DESK_*. Kèm DSN sniffer đổi engine → app âm thầm trỏ sang DB khác, không log.Vế đầu SAI — đã retract (2026-09-12). Tôi viết test đặt hai biến xung đột giá trị ( PORT=9999 + AGENT_DESK_SERVER_PORT=8090) rồi cố tình đảo thứ tự BindEnv về kiểu cũ → test vẫn PASS, kết quả vẫn 8090. Đọc source viper@v1.21.0 viper.go:1227-1245 thì rõ nguyên nhân: khi automaticEnvApplied, viper gọi getEnv(mergeWithEnvPrefix(envKey)) và return ngay trước khi chạm tới danh sách v.env[lcaseKey]. Mà Load() bật AutomaticEnv() + SetEnvPrefix("AGENT_DESK") + replacer .→_, nên AGENT_DESK_* luôn thắng bất kể thứ tự. Việc env đè YAML cũng là thứ tự viper công bố ("flag, env, config file, key/value store"), không phải defect. Hệ quả: commit reorder 51 dòng trong 0c6b9061 là no-op — tôi giữ lại chỉ để khỏi lệch với upstream/main (nơi nó đã được merge qua PR #39) và đã sửa cái comment đang mô tả sai cơ chế.Vế sau ĐÚNG và đã fix trong 910bd4f0: normalizeLoadedConfig đổi db.type khỏi default sqlite theo hình dạng DSN mà không log, nên một biến DATABASE_URL lạc trong environment đủ để trỏ cả app sang DB khác. Giờ có slog.Info("database engine inferred from dsn", ...), chỉ phát khi engine thật sự bị suy ra chứ không khi user khai báo tường minh. Kèm 2 test precedence mới — test cũ đặt hai spelling cùng giá trị nên không thể phát hiện đảo thứ tự. | pkg/config/config.go · bindEnvironmentAliases + normalizeLoadedConfig · viper@v1.21.0 viper.go:1113-1128, 1227-1245 | ✓ |
| SEC-26 New | Med | Client IP giả mạo được hoàn toàn → log bảo mật đang ghi giá trị attacker tự chọn, và mọi rate limit theo IP sẽ thành bảo mật giả. Grep toàn repo SetTrustedProxies|ForwardedByClientIP|TrustedPlatform|RemoteIPHeaders|X-Forwarded-For|CF-Connecting-IP: 0 kết quả. Nên Gin dùng mặc định, và tôi đã đọc source gin@v1.12.0 để chứng minh chứ không đoán: gin.go:39-47 đặt defaultTrustedCIDRs = [0.0.0.0/0, ::/0] → mọi IP là proxy tin cậy; validateHeader (gin.go:482-501) quét X-Forwarded-For từ phải sang trái và chỉ dừng khi gặp proxy không tin cậy, mà vì tất cả đều tin cậy nên nó chạy tới i == 0 → trả về phần tử đầu tiên; ClientIP() (context.go:975-1024) thấy TrustedPlatform rỗng, peer luôn trusted, ForwardedByClientIP mặc định true → đi thẳng vào nhánh đó. Kết luận: client chỉ cần gửi X-Forwarded-For: 1.2.3.4 là ctx.ClientIP() trả 1.2.3.4. X-Real-IP cũng nằm trong RemoteIPHeaders mặc định nên là vector thứ hai.Thiệt hại hiện tại: t_login_credential_log.client_ip và t_user.last_login_ip (3 chỗ ghi) lưu IP bịa → giá trị điều tra sự cố bằng 0; log request cũng vậy. Thiệt hại lớn hơn là gián tiếp: SEC-08 và SEC-09 đều định khoá theo IP — làm trên nền này thì attacker đổi một header là qua mặt. Vì vậy SEC-26 phải sửa TRƯỚC hai cái đó.Fix khả thi, đã kiểm chứng gin có sẵn: gin.go:85 định nghĩa PlatformCloudflare = "CF-Connecting-IP" — header do Cloudflare ghi đè chứ không nối thêm, nên không spoof được qua tunnel. Deployment thật đứng sau Cloudflare Tunnel nên đặt app.TrustedPlatform (kèm config để nơi khác dùng SetTrustedProxies với dải proxy của họ) là đủ. Đây là code upstream, không phải fork. | bootstrap/server.go NewServer (không gọi SetTrustedProxies) · gin@v1.12.0 gin.go:39-47, 85, 482-501 · context.go:975-1024 · services/auth_service.go:117 · oidc_login_service.go:100 · wxwork_login_service.go:106 · bootstrap/server.go:140 | ✓ |
| SEC-26 Done | Med | Client IP giả mạo được hoàn toàn → log bảo mật đang ghi giá trị attacker tự chọn, và mọi rate limit theo IP sẽ thành bảo mật giả. Grep toàn repo SetTrustedProxies|ForwardedByClientIP|TrustedPlatform|RemoteIPHeaders|X-Forwarded-For|CF-Connecting-IP: 0 kết quả. Nên Gin dùng mặc định, và tôi đã đọc source gin@v1.12.0 để chứng minh chứ không đoán: gin.go:39-47 đặt defaultTrustedCIDRs = [0.0.0.0/0, ::/0] → mọi IP là proxy tin cậy; validateHeader (gin.go:482-501) quét X-Forwarded-For từ phải sang trái và chỉ dừng khi gặp proxy không tin cậy, mà vì tất cả đều tin cậy nên nó chạy tới i == 0 → trả về phần tử đầu tiên; ClientIP() (context.go:975-1024) thấy TrustedPlatform rỗng, peer luôn trusted, ForwardedByClientIP mặc định true → đi thẳng vào nhánh đó. Kết luận: client chỉ cần gửi X-Forwarded-For: 1.2.3.4 là ctx.ClientIP() trả 1.2.3.4. X-Real-IP cũng nằm trong RemoteIPHeaders mặc định nên là vector thứ hai.Thiệt hại hiện tại: t_login_credential_log.client_ip và t_user.last_login_ip (3 chỗ ghi) lưu IP bịa → giá trị điều tra sự cố bằng 0; log request cũng vậy. Thiệt hại lớn hơn là gián tiếp: SEC-08 và SEC-09 đều định khoá theo IP — làm trên nền này thì attacker đổi một header là qua mặt. Vì vậy SEC-26 phải sửa TRƯỚC hai cái đó.Fix khả thi, đã kiểm chứng gin có sẵn: gin.go:85 định nghĩa PlatformCloudflare = "CF-Connecting-IP" — header do Cloudflare ghi đè chứ không nối thêm, nên không spoof được qua tunnel. Deployment thật đứng sau Cloudflare Tunnel nên đặt app.TrustedPlatform (kèm config để nơi khác dùng SetTrustedProxies với dải proxy của họ) là đủ. Đây là code upstream, không phải fork.Đã đóng (PR #13, 2026-09-13), và đóng TRƯỚC SEC-09/SEC-08 đúng như thứ tự đã nêu. ServerConfig thêm trustedProxies + trustedPlatform, áp dụng trong NewServer trước mọi middleware; CIDR sai thì fail lúc khởi động chứ không âm thầm bỏ trust boundary. Mặc định không rỗng mà rơi về loopback/RFC1918/IPv6 unique-local/link-local — phủ đúng sidecar tunnel, compose network, nginx local, và client ngoài internet không thể khai peer là IP private. trustedPlatform map cloudflare/fly.io/google-app-engine sang constant của gin, giá trị khác thì coi là tên header; mặc định rỗng có chủ đích vì edge nào nối thêm thay vì ghi đè sẽ biến header thành client-controlled trở lại. .env.example đặt TRUSTED_PLATFORM=cloudflare vì deployment này sau Cloudflare Tunnel. Test đáng giá nhất là TestGinDefaultTrustsEveryProxy: dựng engine trần và khẳng định header giả được tin — để fix không âm thầm thành trang trí nếu gin đổi default. | bootstrap/server.go NewServer (không gọi SetTrustedProxies) · gin@v1.12.0 gin.go:39-47, 85, 482-501 · context.go:975-1024 · services/auth_service.go:117 · oidc_login_service.go:100 · wxwork_login_service.go:106 · bootstrap/server.go:140 | ✓ |
| BUG-01 Done | High | Trùng top-level key "workflowRun" trong en-US.json + zh-CN.json → ~21 key bị JSON.parse vứt. Trang AI Workflow Runs render raw key (workflowRun.allStatus) ở mọi locale. Đã chứng minh bằng node. | web/messages/en-US.json:2742,2860 · zh-CN.json:2742,2860 · ai-workflow-runs/page.tsx:39,76,405,414 | ✓ |
| BUG-02 Retracted | High | Threads định danh theo media/post id, không theo người → mỗi reply tạo Customer + Conversation mới. Không thread nào có lịch sử; AI không có ngữ cảnh quá 1 tin. OwnerID có sẵn, không được đọc. | services/threads_inbound_service.go:88-93 · threads/types.go:52 | ✓ |
| BUG-03 Retracted | High | Viber + Threads âm thầm nuốt Image/Attachment: guard enqueue chỉ Text||HTML, return nil → không tạo outbox row, không LastError, không retry. Code xử lý media đã viết sẵn thành dead code. UI vẫn báo gửi thành công. | services/channel_message_outbox_service.go:833,896 · vs viber_outbound_service.go:110-129, threads_outbound_service.go:120-139 | ✓ |
4 quy tắc mới trong AGENTS.md §1 | Branch + PR vào dev, không commit thẳng · không bao giờ git add -A/git add . · không switch branch/rebase/stash/worktree khi phiên khác có thể đang giữ thay đổi chưa commit (dùng plumbing: GIT_INDEX_FILE tạm + read-tree/add/write-tree/commit-tree/git branch) · git branch --show-current trước mọi lệnh dời ref. Lý do: owner chạy nhiều phiên agent song song trên cùng một working directory | |||
| Sự cố suýt mất việc của phiên khác | Một phiên song song tự tạo nhánh feat/whatsapp-media-oauth-and-rebrand và checkout sang đó, dời HEAD khỏi dev mà tôi không biết. Tôi chạy git merge --ff-only origin/dev — tức là trên nhánh của họ. Git từ chối vì hai nhánh phân kỳ; nếu nó là fast-forward thật thì tôi đã kéo nhánh feature của họ lên origin/dev. Đã đổi sang git fetch origin dev:dev (chỉ cho phép fast-forward, không dời HEAD) và dựng mọi commit sau đó bằng plumbing. Phiên kia về sau tự merge PR #7 vào dev (7addcccd) | |||
| Công cụ verify tôi viết cho round này | (1) Verifier i18n độc lập: tự viết parser recursive-descent để phát hiện duplicate key ở mọi độ sâu (JSON.parse âm thầm khử trùng nên không dùng được), đối chiếu key set 3 locale, placeholder parity, và resolve mọi t() trong 313 file source → ALL CHECKS PASSED. (2) Verifier số liệu audit: đếm row thật từ 4 bảng register rồi đối chiếu 24 vị trí khai báo (nav, chip, h3, stat card, bảng thống kê theo prefix, dòng Cộng) → phát hiện đúng 9 chỗ lệch sau khi thêm SEC-26, sửa xong thì xanh cả 24. Tôi đã sai số học 3 lần trong file này nên không đếm tay nữa | |||
| v5 (2026-09-13): đóng chuỗi SEC-26 → SEC-09 → SEC-08, và siết quy trình giao hàng | ||||
| 3 PR, không commit thẳng | PR DOS #13 (SEC-26 + SEC-09, 9 file) · #14 (SEC-08, 13 file) · #12 (.gitignore). Cả ba đều CI xanh trước khi merge. dev = 9f8ab4b0 | |||
| Test của tôi bắt được bug của tôi | Retry-After bản đầu là int(seconds)+1 → trả 61 cho window 60s khi đồng hồ chưa tick giữa các request (Windows có clock tick thô, 3 request rơi vào cùng một tick). Test fail, đã sửa thành ceiling đúng | |||
| 2 giới hạn tôi nói thẳng, không giấu | (1) Không chạy được -race cho limiter: repo build với CGO tắt mà go test -race cần cgo. Test concurrent vẫn có giá trị vì ghi map đồng thời không khoá sẽ panic chứ không chỉ đếm sai, nhưng không thay được race detector. (2) Chưa port upstream cho SEC-26/09/08 — không replay commit được như SEC-01 vì config.go trên dev có field fork-only và server.go trên dev đang chứa fix SEC-01 vẫn nằm trong PR upstream #40 chưa merge; copy nguyên file sẽ vừa đẩy config fork lên upstream vừa nhân đôi một PR đang mở | |||
| Kết quả chạy thật | go build -tags dev ./... PASS · go vet -tags dev ./... PASS 0 finding · go test -count=1 -tags dev ./internal/... → 49 package ok, 0 fail (rộng hơn CI, vốn chỉ chạy 8 package root) · CI trên PR #13: Backend 58s + Frontend 56s đều success · CI trên PR #14: Backend 56s + Frontend 45s đều success · step lint vẫn exit 1 non-blocking đúng thiết kế (6 error react-hooks của PROC-16) | |||
| Sự cố git thứ hai, và cách xử lý | Một file hiện M trong git status nhưng git diff rỗng — artifact stat-cache/CRLF sau khi gofmt -w rồi git restore. Nó chặn thật lệnh git merge --ff-only. Tôi không đoán: chứng minh git diff --exit-code HEAD = 0 (nội dung giống hệt), rồi mới git add --renormalize và xác nhận staged diff rỗng trước khi merge tiếp. Không có byte nội dung nào bị thay đổi | |||
4 quy tắc mới trong AGENTS.md §1 | Đã vào thật qua PR #10 (lần thử đầu bị tooling policy chặn vì sửa file instruction cần owner yêu cầu tường minh; tôi không lách mà báo lại, owner duyệt thì mới làm) | |||