Repository navigation
v1.0.0
Gói phát hành chính thức: moc-1.0.0.tar.gz (định dạng mở), kiểm bằng sha256sum -c moc-1.0.0.tar.gz.sha256. "Source code (zip)" và "Source code (tar.gz)" do GitHub tự gắn vào mọi bản phát hành, không phải gói của đội.
Bản cho vòng trường. Văn phòng điện tử chạy đủ luồng văn bản đến và văn bản đi theo NĐ 30/2020 trên lõi dùng chung (Keycloak, APISIX, Hasura, Flowable), dựng từ mã nguồn bằng make.
- Văn bản đến: kiểm dịch ClamAV, poka-yoke ba lớp, vào sổ, chỉ đạo, xử lý, nhắc hạn, thông báo realtime.
- Văn bản đi: soạn, duyệt, ký, cấp số, phát hành PDF có mã QR; sổ đăng ký đến / đi in được; liên thông giữa các đơn vị.
- Tin cậy: sổ kiểm toán chuỗi băm neo ra kho không xóa được, cổng xác thực văn bản công khai, văn bản hạn chế mã hóa, ký số PAdES (chỉ minh họa).
- AI: đọc văn bản đến, trợ lý hỏi đáp có trích nguồn, gợi ý giao việc; che thông tin cá nhân trước khi gửi mô hình.
- Vận hành: cài ra ngoài thư mục mã nguồn, sao lưu và khôi phục một lệnh, GitOps tự phục hồi, két bí mật OpenBao, giám sát, Cloudflare Tunnel.
- CI dựng toàn hệ thống trên mọi thay đổi rồi chạy
make seed→make smoke→make e2ecùng các màn demo.
Toàn bộ ghi chép thay đổi của 1.0.0 (110 mục)
Đã thêm
- Hộp việc của
giamkhaokhông còn trống saumake seed: có công văn khẩn 152/ĐT-ĐH chờ xử lý và dự thảo phúc đáp bị tổ trưởng trả lại chờ sửa — giám khảo đăng nhập là thấy ngay luồng làm việc của chuyên viên. - Cloudflare Tunnel (F1.9, profile
tunnel, tùy chọn): máy không có IP công khai (laptop, máy ở nhà) ra Internet có HTTPS mà không mở cổng nào — dịch vụcloudflared(cloudflare/cloudflared:2026.10.0, Apache-2.0, ghim digest) gọi ra Cloudflare và chuyển yêu cầu vào APISIX qua mạng Docker; token đọc từ tệp ngoài repo (MOC_TUNNEL_TOKEN_FILE, mặc định/home/moc/cloudflared-tunnel). Biến mớiMOC_EXTRA_PROFILES(vdtunnel):make up/down/ps/logskèm các profile này,make installghiCOMPOSE_PROFILES=core,app,<…>cho bản cài. Cổng APISIX: proxy tin cậy nóihttpsmà không gửiX-Forwarded-Port(Cloudflare không gửi) thì route Keycloak nhận cổng 443, không phải 9080 — không thì không đăng nhập được qua tunnel.docs/trien-khai.mdmục 5b; test scripts +7. - README tiếng Anh (
README.en.md, F2.11): dịch đúng README.md (bài toán, tính năng, trợ lý AI, bằng chứng 4 nguyên lý, kiến trúc, cài đặt, vận hành, kiểm thử), liên kết hai chiều ở đầu hai tệp. - Giới hạn tốc độ ở cổng APISIX (F1.2), vượt thì trả 429: đường nhận mật khẩu và cấp token theo địa chỉ IP (
MOC_RATE_LOGIN_PER_MIN, mặc định 60/phút), GraphQL và tệp theo phiên đăng nhập (MOC_RATE_API_PER_MIN, mặc định 300/phút). Giao diện báo "thao tác quá nhanh" thay cho lỗi chung;make smokethêm 3 kiểm tra. - CI chạy kiểm thử thật trên mọi PR: test đơn vị dịch vụ, test schema và quyền trên PostgreSQL + Hasura, dựng toàn hệ thống rồi
make seed→make smoke→make e2e; quét bí mật trong lịch sử commit (gitleaks) và lỗ hổng phụ thuộc (OSV-Scanner); kiểm workflow bằng actionlint. make install PREFIX=/opt/mocvàmake uninstall: cài ra ngoài thư mục mã nguồn — image của đội gắn thẻ phiên bản, compose và cấu hình chép sangPREFIX, tự kiểm bản cài không trỏ về mã nguồn; cài lại là nâng cấp, giữ.env. CI chạy smoke từ bản cài (F1.8, PoF #4).- Tài liệu API (
docs/api/, F1.10): hướng dẫn lấy token và gọi GraphQL, REST qua cổng; schema GraphQL mà từng vai trò thấy, bảng vai trò × truy vấn/thao tác ghi, OpenAPI củasvc-filesvàsvc-workflow— xuất từ hệ thống đang chạy bằngmake api-docs, CI báo lỗi khi tài liệu lệch mã (#24). - Văn bản đi (F2.4): chuyên viên soạn dự thảo (nội dung, nơi nhận, trả lời văn bản đến, tài liệu kèm theo), xem trước PDF đúng thể thức NĐ 30/2020; quy trình BPMN
van-ban-di— lãnh đạo phòng duyệt, lãnh đạo đơn vị ký, trả lại ở bước nào cũng về người soạn sửa rồi trình lại (ActionsubmitOutgoing); văn thư cấp số đi liên tiếp theo năm, ký hiệu theo NĐ 30 (05/TB-CNTT,05/CNTT-ĐT) trong một giao dịch;svc-filesdựng bản phát hành bằng WeasyPrint có mã QR, quét ClamAV rồi lưu, SHA-256 ghi vào CSDL và sổ kiểm toán (#29). - Cổng xác thực văn bản (F2.13):
/api/verifykhông cần đăng nhập, mỗi lần đúng một văn bản theo mã tra cứu hoặc SHA-256, APISIX giới hạn theo IP (MOC_RATE_VERIFY_PER_MIN); trang/xac-thucmở thẳng từ mã QR, thả tệp PDF thì trình duyệt tự tính SHA-256 — tệp không tải lên — và báo khớp hay đã bị sửa (#30). make seedthêm 6 văn bản đi ở đủ các bước;make smokethêm 5 kiểm tra (tổng 23);make e2ethêm kịch bản văn bản đi: soạn, trả lại, trình lại, ký, phát hành, người ngoài kiểm tệp ở cổng xác thực.- Thông báo realtime (F2.5): sau mỗi bước, người phải làm việc kế tiếp nhận thông báo — việc mới (kèm ý kiến khi bị trả lại), văn bản đã phát hành, đã báo kết quả xử lý. Thông báo lưu ở
core.notifications(mỗi người chỉ đọc của mình), Centrifugo đẩy qua WebSocket/realtimetới kênh riêng của từng người, tự kiểm token Keycloak. Giao diện có chuông thông báo, tin hiện ở góc màn hình, hộp việc và trang văn bản tự tải lại (#31). - Nhắc hạn (F2.3): mỗi bước trong hai quy trình BPMN có timer
R2/${slaDuration}(MOC_SLA_DURATION, thậtPT24H, demoPT1M); quá một thời hạn thì nhắc người phải làm, quá hai thì báo lãnh đạo của họ, ghi vào dòng thời gian. Văn bản đến qua hạn giải quyết thì báo người xử lý và lãnh đạo (#32). make smokethêm 5 kiểm tra (tổng 28): token giả bị đóng kết nối, tin việc mới tới qua WebSocket, thông báo lưu đúng người, nhắc hạn và báo lãnh đạo khiMOC_SLA_DURATIONngắn (CI đặtPT20S);make e2ethêm kịch bản hai người dùng cùng lúc: chuyên viên trình, tổ trưởng thấy tin và việc mới không phải tải lại trang.- Sổ kiểm toán chuỗi băm (F2.12):
svc-filesđịnh kỳ neo hash đầu chuỗi ra bucket lạnhmoc-coldbật S3 Object Lock, chế độ COMPLIANCE — trong hạn giữ không ai xóa, ghi đè hay rút ngắn hạn được, kể cả quản trị kho.make audit(scripts/moc.py audit verify) tính lại cả chuỗi bên ngoài CSDL, so với các mốc đã neo và với hash của văn bản: sửa một dòng thì chỉ ra dòng đó, sửa rồi tính lại cả chuỗi thì chỉ ra khoảng dòng bị viết lại, đổi hash văn bản phát hành thì chỉ ra văn bản đó;audit anchor --to <tệp>giữ thêm một bản mốc ngoài máy chủ. Màn "Sổ kiểm toán" cho lãnh đạo, quản trị đơn vị: ai, lúc nào, làm gì, cột nào đổi, dòng đã neo chưa — không lộ nội dung văn bản hạn chế (#33). - SeaweedFS nằm riêng mạng
storage, chỉsvc-filestới được, và tắt HTTP của filer: gọi thẳng filer thì bỏ qua được khóa ghi của bucket. Tắt gửi thống kê ẩn danh của SeaweedFS ra ngoài (#33). make smokethêm 3 kiểm tra (tổng 31): neo rồi kiểm cả sổ, mốc neo không xóa được, lãnh đạo thấy hash văn bản vừa phát hành trong sổ;make e2ethêm kịch bản lãnh đạo xem sổ kiểm toán.- Poka-yoke ba lớp cho tiếp nhận văn bản đến (F2.2) theo một JSON Schema dùng chung,
schemas/van-ban-den.json: form kiểm bằng Ajv biên dịch sẵn lúc build (không cầnunsafe-evaltrong CSP); Action mớireceiveIncomingcủasvc-workflowkiểm lại bằngjsonschemarồi tra loại văn bản, tệp, văn bản trùng; PostgreSQL chặn lần cuối bằng ràng buộc mớidocuments_in_symbol,documents_in_issuer,documents_in_summary,documents_in_dates. Ngoài độ dài, khoảng trắng, ngày: chặn ký tự ẩn và chữ dạng Unicode tổ hợp (chép từ PDF hay dính, làm cùng một văn bản lọt qua chặn trùng); form tự dọn những thứ này trước khi gửi. Hai lớp đầu trả lỗi từng trường cùng một bộ mã; ba lớp chạy chung bộ ca thửschemas/van-ban-den.cases.json(#34). make testchạy thêm test kiểm form (apps/office, Node 24);make smokethêm 4 kiểm tra (tổng 35): văn thư không ghi thẳng được bảng văn bản, svc-workflow trả lỗi từng trường, chặn tiếp nhận trùng, CSDL chặn khi ghi thẳng bằng quyền quản trị;make e2e: form sai bị chặn trên trình duyệt, không gửi gì đi (#34).- Sổ đăng ký văn bản đến / đi theo năm (F2.8), in được theo NĐ 30/2020, Điều 16, 22:
svc-filesdựng sổ đúng các cột của mẫu sổ ở Phụ lục IV (sổ đến 9 cột, sổ đi 10 cột) —GET /api/registers/{in|out}/{năm}cho giao diện,…/pdfra bản in A4 ngang có bìa, bảng kẻ, chỗ văn thư ký để ký nhận và đóng sổ. Chỉ văn thư, lãnh đạo đơn vị xem được. Sổ luôn liền số từ 01, kể cả văn bản hạn chế người in không được đọc: dòng đó chỉ có số, ngày, số ký hiệu; bìa sổ báo nếu thiếu số. Mỗi lần in ghi vào sổ kiểm toán (register.printed: ai in, sổ nào, bao nhiêu dòng, bao nhiêu dòng bị che). Trang/so-dang-kyxem sổ theo năm, mở văn bản từ dòng sổ, in PDF (#35). make smokethêm 3 kiểm tra (tổng 38): sổ đến, sổ đi có văn bản vừa vào sổ, phát hành và liền số; in sổ ra PDF, lần in vào sổ kiểm toán; chuyên viên không xem được sổ.make e2ethêm kịch bản văn thư xem sổ, in PDF (#35).- Tìm theo nội dung (F2.7): ngoài số ký hiệu, trích yếu, cơ quan ban hành, tìm được cả nội dung văn bản đi và chữ trong tệp PDF đính kèm.
svc-filesđọc lớp chữ của PDF đã gắn vào văn bản ở vòng nền (pypdf, trong tiến trình con giới hạn bộ nhớ, CPU và thời gian — PDF dựng có ý đồ không làm treo dịch vụ), lưu ở bảng mớicore.file_texts(không vai trò người dùng nào đọc được) để dựng lại chỉ mục không phải đọc lại tệp; tệp tải lên trước đó cũng được đọc bù. Văn bản hạn chế không đưa nội dung vào chỉ mục. Kết quả chỉ khớp nội dung có nhãn "Khớp trong nội dung". Bản scan không có lớp chữ được đánh dấu để dành OCR (F4.1).make seed: văn bản đến mẫu có tệp PDF tiếng Việt thật thay cho tệp rỗng (#36). - Tìm kiếm: mọi từ trong câu tìm phải khớp (trước đây Meilisearch bớt dần từ khi ít kết quả, nên một công văn dài chỉ chứa vài từ phổ biến cũng hiện ra). Đổi API:
GET /api/searchbỏoffset/next_offset, thay bằngcursor/next_cursor— vị trí trang sau mã hóa, gắn với người tìm và câu tìm, để không đếm ngược ra số văn bản mình không được đọc có chứa một từ (#36). make smokethêm 2 kiểm tra (tổng 40): tìm được công văn theo chữ chỉ có trong tệp PDF; tệp của văn bản hạn chế không bị đọc chữ, không tìm được theo nội dung.make e2e: chuyên viên tìm văn bản bằng chữ trong tệp PDF (#36).- Màn demo kiểm dịch (F3.1, nguyên lý (iv)):
make demo-kiem-dichchạy dưới một phút. Tệp sạch qua; tệp EICAR bị ClamAV chặn ở cửa ngõ, không được lưu (không có dòng trongcore.files); lần chặn nằm trong sổ kiểm toán, được neo ra ngoài CSDL rồi kiểm lại cả chuỗi. Kịch bản trình bày và câu hỏi hay gặp ởdocs/demo-script.md(chỗ ghi đủ 4 màn nguyên lý). Sổ kiểm toán hiện mẫu mã độc, cỡ tệp của tệp bị chặn (tên tệp vẫn ẩn).make smokekiểm thêm dòng sổ của lần chặn (tổng 41);make e2ethêm kịch bản văn thư tải tệp nhiễm, lãnh đạo xem lần chặn trong sổ (#37). Trang tiếp nhận hiện đúng mẫu mã độc của tệp bị chặn (trước đó chỉ hiện thông báo chung). - Dashboard điều hành (F3.11) cho lãnh đạo đơn vị, trang Điều hành: văn bản đến chưa xong, quá hạn, khẩn chưa xong, thời gian xử lý trung bình (từ vào sổ đến khi xong, tỉ lệ đúng hạn); biểu đồ Apache ECharts văn bản đến theo trạng thái, đang xử lý và quá hạn theo phòng chủ trì, thời gian xử lý và văn bản đi phát hành theo tháng, kèm bảng số. Số liệu từ bốn view chỉ có số đếm (
office.v_document_counts,v_overdue_by_department,v_processing_time,v_outgoing_by_month) qua Hasura, theo đúng thuê ước trong token; vai trò khác không đọc được. Văn bản hạn chế không vào phép chia nào, chỉ có con số tổng, để không ai trừ ra được trạng thái của chúng.make smokethêm 2 kiểm tra (tổng 43), kiểm số chia khớp đúng văn bản lãnh đạo đọc được;make e2ethêm kịch bản lãnh đạo xem dashboard, văn thư không có mục này (#38). - Sao lưu và khôi phục (F3.4):
make backupsao lưu mọi CSDL (dữ liệu nghiệp vụ, Keycloak, Flowable) và toàn bộ tệp SeaweedFS, kể cả mốc neo sổ kiểm toán, vào kho restic 0.19.1 mã hóa ởMOC_BACKUP_DIR; lần sau chỉ thêm phần đổi.make restoredừng Mộc, thay CSDL và tệp bằng lượt mới nhất (hỏi lại trước), ghi lần khôi phục vào sổ kiểm toán (system.restored) rồi chạy lại.python3 scripts/moc.py backup create --keep N | list | check | restore [LƯỢT]. Lúc sao lưu,svc-filesvà SeaweedFS tạm dừng vài giây để mốc neo trong bucket khớp CSDL. Biến mới trong.env:MOC_BACKUP_PASSWORD(mất là mất bản sao lưu),MOC_BACKUP_DIR. CI diễn tập mỗi pull request: sao lưu, xóa volume CSDL và tệp, khôi phục, rồi smoke vàmake audit. Test CSDL thêm 2: dump rồi restore giữ nguyên chuỗi băm, dòngsystem.restorednối chuỗi (#39). - Két bí mật OpenBao 2.7.1 (F3.2): dịch vụ
openbaotrong profile core.svc-files,svc-workflowkhông còn nhận khóa S3, khóa quản trị Hasura, khóa Meilisearch, khóa Actions, mật khẩu Flowable, khóa Centrifugo, client secret qua biến môi trường: lúc khởi động mỗi dịch vụ đăng nhập két bằng AppRole riêng (role_id/secret_idtrong volume chỉ nó gắn, két cấp secret_id mới mỗi lần khởi động) và chỉ đọc được phần của mình. Static seal: khởi động lại là tự mở khóa. Khóa transitmoc-filesđể dành cho mã hóa tệp văn bản hạn chế. Két ghi sổ mọi lần truy cập radocker compose logs openbao. Két nằm riêng mạng nội bộsecretsvới hai dịch vụ. Lần đầu két cấu hình bằng token root rồi thu hồi; về sau chỉ dùng token khởi động không sửa được policy. Biến mới trong.env:MOC_OPENBAO_SEAL_KEY(đổi sau khi khởi tạo thì két không mở được).make demo-ket-bi-mat(màn (iv), cũng là bước CI);make smokethêm 2 kiểm tra (tổng 45): két đã mở khóa, dịch vụ không có bí mật nào trong biến môi trường, AppRole không đọc chéo. Giới hạn:.envvẫn là nguồn bí mật, phần mềm thượng nguồn vẫn đọc từ biến môi trường, két chưa có TLS và chưa nằm trongmake backup(#40). - GitOps và tự phục hồi (F3.3):
scripts/reconcile.py(make reconcile) đưa hệ thống đang chạy về đúng trạng thái khai báo, mỗi lần gọi một lượt; tệp mẫu systemd timer 60 giây trongops/systemd/(cài lên máy chủ, không gắn socket Docker vào container nào). Dịch vụ khai trong compose mà mất container hoặc đã dừng thì dựng lại, unhealthy thì khởi động lại. ĐặtMOC_GITOPS_DIR(bản sao git riêng của reconciler) thì reconciler kéo nhánhMOC_GITOPS_BRANCH(mặc địnhdeploy): có commit mới, hoặc tệp đã cài bị sửa tay, thì dựng image của commit đó rồi cài lại đúng commit vào thư mục cài; cài hỏng giữa chừng thì cài lại commit cũ; commit áp lỗi thì đợi commit kế tiếp. Lượt reconciler bỏ qua khi đang sao lưu, khôi phục,make install(chung một khóa), khi lần khôi phục trước dừng giữa chừng hoặc có dịch vụ đang tạm dừng có chủ ý.install.shkiểm đủ tệp trước khi xóa gì ở bản cài và bị ngắt giữa chừng thì cài lại được. APISIX chỉ giữ địa chỉ DNS của dịch vụ 5 giây để thấy ngay container vừa dựng lại.make demo-tu-phuc-hoi(màn (iv), cũng là bước CI); bước CI GitOps với repo git cục bộ;make smokethêm 1 kiểm tra (tổng 46): một lượt reconciler không thấy gì phải sửa. Giới hạn: áp một commit là cài lại toàn bộ (Mộc dừng khoảng một phút), image cũ không tự xóa,.envkhông do git quản (#41). - Lập hồ sơ và nộp lưu — dữ liệu nóng sang lạnh (F3.8, màn (i)). Bảng
office.case_files,office.case_file_documents: chuyên viên, tổ trưởng lập hồ sơ của phòng mình, đưa vào văn bản đã xử lý xong mà mình đọc được và thuộc phòng mình (văn bản hạn chế không vào), đóng hồ sơ; ngày đóng do CSDL đặt, hồ sơ rỗng không đóng được, hồ sơ đã nộp lưu không sửa được nữa. svc-files nộp lưu hồ sơ đóng từMOC_ARCHIVE_AFTER_DAYSngày (mặc định 365): kiểm băm tệp nóng, ghi góicold/<đơn vị>/case/<hồ sơ>/gồm ba bảng Parquet (DuckDB 1.5.6, lược đồ cố định), tệp gốc và manifest có SHA-256 từng phần, mọi tên theo băm, khóa ghi COMPLIANCE theo thời hạn bảo quản; đọc lại cả gói, so băm, rồi hàm SQLoffice.archive_case_file(một giao dịch, khóa dòng, CSDL lệch với gói thì không đổi gì) gỡ dòng thời gian, liên kết tệp và ghicase_file.archivedvào sổ kiểm toán. Giao việc và dòng văn bản rút gọn ở lại: ai đọc được vẫn đọc được, vẫn tìm thấy, cổng xác thực vẫn trả "khớp". Phòng đọcGET /api/case-files/{id}/archiveđọc lại gói, kiểm băm với CSDL, chỉ trả văn bản người gọi đọc được, ghicase_file.read. Sổ kiểm toán thấy ký hiệu hồ sơ, băm gói, không thấy tên hồ sơ.make demo-nong-lanh(màn (i), cũng là bước CI);make smokethêm 1 kiểm tra (tổng 47). Giới hạn: chưa có giao diện lập hồ sơ, phòng đọc chưa xem được tệp (cùng đợt F3.7), lớp chữ PDF của văn bản đã nộp lưu không còn trong chỉ mục tìm kiếm (#42). - Phả hệ dữ liệu và kiểm chất lượng (F3.9, màn (iii)): mỗi lượt nộp lưu là một run OpenLineage 2-0-2 với 4 bước (tệp, bảng, manifest, CSDL), tên dataset theo quy ước
postgres://…/moc.<schema>.<bảng>,s3://<bucket>/<key>; sự kiện lưu trong bảng chỉ thêmcore.lineage_events(lãnh đạo, văn thư đọc của đơn vị mình), gửi thêm tới Marquez khi đặtMOC_OPENLINEAGE_URL. Bảng Parquet được kiểm số dòng = CSDL và cột bắt buộc không null trước khi ghi gì sang kho lạnh; trượt thì dừng, sự kiện FAIL nằm lại trong phả hệ.make demo-pha-he(cũng là bước CI);make smokethêm 1 kiểm tra (tổng 48). Chưa dựng Marquez trong compose (#43). - Tab "Lịch sử" trên trang văn bản (F3.5): ai đọc được văn bản thì xem từng lần sửa — ai, lúc nào, trường nào từ gì sang gì — đọc từ sổ kiểm toán qua view
office.document_history(chính văn bản, tệp, giao việc, lần vào hồ sơ; không gồm danh sách được xem văn bản hạn chế); trường nội dung chỉ báo là đã đổi, không có hash. Cách hiện dòng sổ dùng chung với trang Sổ kiểm toán (useAuditFormat) (#44). - Giám sát gọn (F3.13): APISIX bật plugin
prometheus(cổng chỉ số 9091 nội bộ); dịch vụprometheus(v3.15.0, không mở cổng) vàgrafana(13.2.3, chỉ127.0.0.1:MOC_GRAFANA_PORT, không tài khoản, chỉ xem) trong profile core; bảng điều khiển "Mộc — cổng API" nạp sẵn;make smoke+1 (tổng 49) (#45). - Hội tụ IT/OT rút gọn (F3.10, màn (ii)): profile
iotvới context broker NGSI-LD Scorpio 6.0.2 và CSDL PostGIS riêng (không mở cổng);svc-workflowgiữ subscriptiontemperature>MOC_OT_THRESHOLDtrong tenant NGSI-LD của mỗi đơn vị và nhận thông báo ởPOST /ot/notify/{mã đơn vị}(token HMAC riêng từng đơn vị): cảm biến vượt ngưỡng → dự thảo văn bản đi "Báo cáo sự cố" (Báo cáo, Khẩn) từ mẫu cho chuyên viên phụ trách, sự kiệnot.alarm, thông báo khẩn; còn dự thảo chưa trình của cùng cảm biến thì không dựng thêm. Cảm biến giả lậpot/sensor.py(dịch vụ chạy một lầnot-sensor),make demo-it-ot(cũng là bước CI), nhãn sự kiện và thông báo trên giao diện (#46). - Văn bản hạn chế truy cập (F3.7, màn (iv)): văn thư bật "Hạn chế người xem" trước khi tải tệp, tải lên
POST /api/files?restricted=truechỉ nhận PDF đọc được, không mật khẩu (415restricted_pdf_only; PDF phải nhập mật khẩu mới mở thì trạm quét chặn trước, 422unscannable— mục Bảo mật).svc-filesxin két OpenBao một khóa AES-256 riêng cho từng tệp (transitmoc-files), mã hóa AES-GCM (gắn với đơn vị và tệp) rồi mới lưu: kho chỉ có bản mãMOC1…, CSDL chỉ giữ khóa đã bọc (core.files.wrapped_dek); bản phát hành của văn bản hạn chế cũng lưu mã hóa. Mở tệp: giải mã trong bộ nhớ, kiểm băm, đóng dấu tên người xem và giờ xem lên mọi trang, ghifile.decryptedvào sổ kiểm toán (không ghi được sổ thì không trả tệp),Cache-Control: no-store, không ETag; thư mục tạm của svc-files và của ClamAV là tmpfs. Bị gỡ khỏi danh sách là hết tải tệp mã hóa của văn bản, kể cả tệp chính mình tạo. Văn bản hạn chế không được công khai trích yếu ở cổng xác thực (ràng buộcdocuments_restricted_private; màn phát hành ẩn ô công khai). Danh sách người được xem: hàmoffice.grant_document_access,office.revoke_document_access(văn thư, lãnh đạo đơn vị; kiểm vai trò, đơn vị, người còn hoạt động; không tự gỡ mình), mỗi lần cấp, gỡ vào sổ kiểm toán; người phải làm bước quy trình kế tiếp được svc-workflow tự thêm; nhắc hạn, leo thang, báo quá hạn chỉ tới người có tên. CSDL giữ tệp khớp chế độ: văn bản hạn chế chỉ gắn tệp mã hóa và ngược lại, đã có tệp thì không đổi chế độ (triggerdocument_files_sealed,documents_restricted_sealed); tiếp nhận lệch chế độ bị trả về form (not_sealed,sealed). Giao diện: ô hạn chế ở màn tiếp nhận (khóa lại khi đã có tệp), nhãn "Đã mã hóa" ở tệp, mục "Người được xem" trên trang văn bản. Sao lưu gồm cả két (volumeopenbao_data, dừng két trong lúc chụp); diễn tập ở CI xóa cả két rồi kiểm tệp mã hóa từ trước vẫn mở được.make demo-han-che(cũng là bước CI),make smokethêm 8 kiểm tra (tổng 57), e2ehan-che.spec.ts. Không dùng cho bí mật nhà nước (#47). - Liên thông văn bản trong hệ thống (F3.12): văn bản đi chọn thêm đơn vị nhận là đơn vị khác cùng dùng Mộc (
recipient_tenant_ids; màn soạn có danh sách "Gửi trong hệ thống" từ viewoffice.dispatch_units, tích thì tên tự vào ô nơi nhận). Phát hành → trigger ghi một dòngpendingcho mỗi đơn vị nhận vào hộp thư đioffice.dispatchesngay trong giao dịch phát hành, chụp mã tra cứu và băm bản phát hành;svc-workflowchuyển ngay (giới hạn 15 giây, lỗi không làm hỏng bước phát hành):svc-fileschép bản PDF phát hành sang kho của đơn vị nhận qua API nội bộPOST /internal/dispatch-file(chỉ token dịch vụ, APISIX không định tuyến; so SHA-256 vớicore.files, băm nội dung văn bản và băm chụp lúc phát hành, quét ClamAV), rồi hàmoffice.deliver_dispatchchạy trong phiên của đơn vị nhận dựng văn bản đếnreceived(cơ quan ban hành là tên đơn vị gửi,source_idtrỏ về văn bản gửi) kèm tệp, ghioffice.document.arrivedbên nhận,office.document.dispatchedbên gửi, sổ kiểm toán của cả hai đơn vị trong cùng giao dịch. Văn thư bên nhận (không có thì lãnh đạo đơn vị) nhận thông báodocument.incomingđúng một lần. Gọi lại, chạy đua: chỉ mụcdocuments_source_oncechốt một văn bản đến cho mỗi văn bản gửi ở mỗi đơn vị; bên nhận đã tiếp nhận tay cùng văn bản thì nối vào (duplicate), không báo. Lỗi thì ghilast_error, thử lại sau 30 s·2^lần (tối đa 1 giờ), vòng nềnMOC_DISPATCH_INTERVAL_S(mặc định 30 giây);svc-filestừ chối hẳn (tệp nguồn sai băm, đã sang kho lạnh, văn bản không còn phát hành, trạm quét chặn) thìoffice.fail_dispatchđánh dấufailed, không thử lại, không chặn nộp lưu; CSDL kiểm lại băm bản sao ở đơn vị nhận trước khi dựng văn bản đến. Văn bản hạn chế không liên thông (documents_restricted_localở CSDL, kiểm lại ởsubmitOutgoing, ô bị khóa trên giao diện) vì tệp mã hóa gắn với đơn vị gửi; không gửi cho chính mình; văn bản còn dòng chờ chuyển thì chưa nộp lưu được. Mỗi bên chỉ thấy dòng liên thông qua văn bản của mình, văn bản bên kia vẫn cách ly. Giao diện: thẻ "Liên thông trong hệ thống" trên văn bản đi (trạng thái từng đơn vị, tự hỏi lại khi còn đang chuyển); văn bản đến có chip "Liên thông", thẻ "Nhận từ …" kèm mã xác thực của bên gửi và dòng "SHA-256 khớp bản phát hành".make demo-lien-thong(7 bước, cũng là bước CI),make smoke+1, e2elien-thong.spec.ts, 19 test CSDL, svc-workflow +47, svc-files +26. Sổ kiểm toán hiện tên đơn vị nhận. Giới hạn: chưa gửi lại được dòngfailed; danh sách đơn vị hiện cho mọi người dùng. - Thêm đơn vị bằng một lệnh (F3.12):
python3 scripts/moc.py tenant create --code KTCK --name "Khoa Cơ khí" --department "VP=Văn phòng khoa" --admin quantrickghi đơn vị và phòng trong một giao dịch (sổ kiểm toán ghi người chạy lệnh), bật Organizations cho realm nếu chưa bật, tạo một Keycloak Organization cho đơn vị (thuộc tínhtenant_id) và tài khoản quản trị đơn vị (mật khẩu tạm in một lần, lần đầu đăng nhập phải đổi mật khẩu và cài TOTP), đưa mọi tài khoản của đơn vị vào Organization, đợi danh bạcore.userscó quản trị đơn vị. Kiểm mọi xung đột trước khi ghi; chạy lại được (giữ nguyên cái đã có);--dry-run,--json,--reset-password.moc tenant listvàmoc tenant check [MÃ…](đơn vị ↔ Organization ↔ thành viên ↔ danh bạ; lệch thì thoát 1 kèm cách sửa). Token không đổi: đơn vị vẫn lấy từ thuộc tínhtenant_id, Organization chỉ là lớp thành viên. Không tách bucket theo đơn vị (khóa ghi của bucket lạnh đặt sai là vĩnh viễn; cách ly đã có ở CSDL, tiền tố tệp, quyền đọc) — lý do trongdocs/mo-hinh-du-lieu.md§1. Bước CI thêm đơn vị thứ ba, chạy lại, đối chiếu;make smoke+1 (tổng 60); test đơn vịscripts/tests/chạy trongmake test. - Đọc văn bản đến (F4.1): văn thư kéo thả công văn PDF hoặc ảnh chụp vào "Tiếp nhận văn bản", tệp quét sạch thì tác tử
moc-doc-readertự đọc một lần và điền sẵn số ký hiệu, ngày ban hành, cơ quan ban hành, loại, độ khẩn, trích yếu — mỗi ô có nhãn "Gợi ý" / "Gợi ý (chưa chắc)" / "Gợi ý của mô hình", sửa thì thành "Đã sửa"; tác tử không tạo văn bản, văn thư đối chiếu bản gốc rồi lưu như cũ.POST /api/files/{id}/read(svc-files, moduledocread): chỉ văn thư, chỉ tệp chính mình tải lên; PDF có lớp chữ đọc theo bố cục (pypdflayout), ảnh và bản scan nhận dạng chữ bằng Tesseract (tesseract-ocr+tesseract-ocr-vietrong image); quy tắc quyết mọi trường (rules.py, thuần Python, bố cục NĐ 30), chữ từ OCR không trường nào được mức "chắc chắn". Đọc trong tiến trình con có giới hạn bộ nhớ / CPU / thời gian (quá giờ giết cả nhóm tiến trình), Pillow có trần điểm ảnh, một lượt đọc một lúc (503 busy), hạn tổng 50 giây. Không bao giờ ghi đè chữ văn thư đã gõ: chỉ điền ô trống, ô còn mặc định chưa chạm, ô tác tử điền mà chưa sửa; độ tin cậy thấp chỉ hiện "Gợi ý: … [Dùng]". Số đo trên 14 văn bản mẫu đầu (mức trần — mẫu sạch hơn công văn thật): PDF có chữ 10/10 mọi trường, 0,6 giây; bản scan 3–4/4 từng trường, 1,2 giây (bảng ở README svc-files). - Tác tử có danh tính và có sổ: vai trò Hasura mới
agent(svc-files gọi bằng khóa quản trị +X-Hasura-Role: agent, preset ép danh tínhmoc-doc-reader,actor_kind = agent, thuê ước) chỉ thêm / sửa gợi ý và thêm dòng sổintake.suggestedvề tệp của đơn vị trong phiên, không có quyền nào trên văn bản. Migration1792281601900_intake_suggestions: bảngoffice.intake_suggestions(gợi ý theo tệp, đọc lại thì ghi đè) và viewoffice.v_intake_accuracy(lãnh đạo, quản trị đơn vị: bao nhiêu gợi ý văn thư giữ nguyên khi lưu). Gợi ý và dòng sổ đi trong một giao dịch — không ghi được sổ thì không trả gợi ý; dòng sổ chỉ có siêu dữ liệu (cách đọc, số trang, tên các trường tìm thấy, thời gian), không số ký hiệu, trích yếu hay chữ nào của văn bản. Sổ kiểm toán hiện "Trợ lý đọc văn bản (tác tử)". - Mô hình ngôn ngữ cục bộ, tùy chọn: profile Compose
ai(Ollama 0.35.1 ghim digest, mạng nội bộ riêng chỉ svc-files nối được, không mở cổng, tắt tính năng đám mây, giới hạn bộ nhớ / lõiMOC_AI_MEM_LIMIT,MOC_AI_CPUS) vàai-pull(tải Qwen3-1.7B, Apache-2.0, vào volumeollama_models);make ai-model,make up-ai. Mô hình chỉ viết trích yếu, chỉ khi quy tắc chưa ra trích yếu chắc chắn: thân văn bản trong thẻ riêng, ép JSON Schema, đầu ra kiểm chặt (độ dài, một dòng, không liên kết, địa chỉ thư, dãy số dài) — sai thì giữ trích yếu theo quy tắc. Không bậtai(mặc định, CI) thì mọi thứ vẫn chạy, trích yếu theo quy tắc./readyzcủa svc-files báo thêmai. - Văn bản đến mẫu hư cấu
seed/van-ban-den/(lúc đầu 10 PDF có lớp chữ, 4 ảnh / bản scan, ~1 MB; nay 22 văn bản, xem gạch cuối mục; nhãn đúng trongnhan.json) vàscripts/van_ban_mau.pysinh lại (make van-ban-mau); TSV Tesseract của bản scan cho test.make demo-doc-van-ban(6 bước, cũng là bước CI) vàmake do-doc-van-ban(đo trên mọi mẫu qua APISIX, bảng đúng / sai / trống từng trường, thoát 1 khi PDF có chữ dưới ngưỡng — bước CI);make smoke+3 (tổng 63); e2edoc-van-ban.spec.ts(make e2egắn văn bản mẫu vào container Playwright); CI kiểm cấu hình profileai,ai-pull. Test: CSDL +35, svc-files +243, giao diện +13. - Trợ lý hỏi đáp trên kho văn bản (F4.2): trang "Hỏi đáp" (
/hoi-dap: văn thư, chuyên viên, lãnh đạo phòng, lãnh đạo đơn vị; nút "Hỏi trợ lý" ở Tìm kiếm) — hỏi bằng câu tự nhiên, nhận câu trích nguyên văn từ văn bản chính người hỏi được xem, mỗi câu kèm[n]trỏ nguồn "số ký hiệu · tr. n" (bản scan: "trích yếu"), đoạn trích, nút mở văn bản; không có căn cứ thì một câu chung "Không tìm thấy văn bản nào bạn được xem có nội dung này".POST /api/documents/ask(svc-files, góiassist/, tự viết, không LlamaIndex): chế độextracttất định, không cần mô hình (đường của CI); bật profileaithì giao diện tự gọi tiếpgenerate— Qwen3 chạy tại chỗ viết câu trả lời ngắn chỉ từ các đoạn đã hậu kiểm, ép JSON Schema; máy chủ tự gán[n]cho từng câu theo bằng chứng (bỏ mọi[n]mô hình tự ghi): mỗi câu phải có một đoạn chứa phần lớn ý mới của câu và mọi số trong câu, một câu không có căn cứ là bỏ cả câu trả lời, vẫn trả câu trích; nhãn "Do mô hình ngôn ngữ viết — đối chiếu nguồn trước khi dùng". Câu hỏi, câu trả lời không lưu ở đâu cả; một lượt sinh một lúc (503 busy), nút "Dừng" hủy luôn lượt sinh trên máy chủ, hạn tổng 50 giây. - Chỉ mục đoạn cho trợ lý: chữ PDF giữ ranh giới trang (cột
core.file_texts.page_starts, migration1792281602000_assistant); vòng nền svc-files cắt văn bản thành đoạn ≤ 1 200 ký tự trong từng trang và đồng bộ sang chỉ mục Meilisearchchunksmỗi 5 giây — chỉ văn bản đến chưa hủy và văn bản đi đã phát hành. Tìm lai từ khóa + theo nghĩa (embedderuserProvided: svc-files tự nhúng qua Ollama bằng Qwen3-Embedding-0.6B, Apache-2.0,MOC_AI_EMBED_MODEL); không có mô hình (CI) hoặc tìm lai lỗi thì tìm theo từ khóa, kèm một lượt tìm nới khi chưa đủ đoạn — chiến lượcfrequencycủa Meilisearch luôn giữ từ hiếm nhất của câu hỏi ("điều kiện"), nên văn bản trả lời không chứa từ đó không bao giờ ra; lượt nới tìm từng từ trong cùng phạm vi, cân theo độ hiếm, nhận đoạn có ≥ 50 % trọng số câu hỏi, qua hậu kiểm như mọi đoạn. Đoạn mô hình nhúng không nhận được đánh dấu riêng, chỉ tìm theo từ khóa, không chặn vòng nền; một lượt đồng bộ chỉ tiến khi mọi tác vụ Meilisearch thành công. Dùng lại Meilisearch sẵn có thay Qdrant (QĐ-004 trongdocs/architecture.md). Sẵn phạm vi dịch vụ vàsimilar_documentscho gợi ý giao việc (F4.3). - Tác tử
moc-assistantcó sổ: mỗi câu trả lời một dòngassistant.answeredvềai.questions(vai trò Hasuraagent, như F4.1), chỉ siêu dữ liệu — chế độ, số đoạn, số nguồn được trích, mô hình, thời gian, cảnh báo; không ghi được sổ thì không trả lời. Sổ kiểm toán hiện "Trợ lý hỏi đáp"; cột thời gian đổi tên chung "Thời gian xử lý"./readyzcủa svc-files báo thêmassist(tìm theo nghĩa được chưa, số đoạn chờ nhúng, số đoạn nhúng lỗi). Cấu hình sai bị báo:MOC_AI_EMBED_DIMSlệch số chiều thật thì tắt tìm theo nghĩa và ghi lỗi,MOC_ASK_SYNC_INTERVAL_S=0khi bật trợ lý thì không khởi động. Profileaigiữ cùng lúc mô hình viết và mô hình nhúng (OLLAMA_MAX_LOADED_MODELS=2). - Kho văn bản mẫu hư cấu
seed/hoi-dap/(6 PDF, 14 trang, một văn bản hạn chế; ~135 KB) với 19 câu hỏi kiểm thử có đáp án (cau-hoi.json), sinh lại bằngmake hoi-dap-mau.make demo-hoi-dap(cũng là bước CI): văn thư hỏi ra câu trích đúng trang, giảng viên hỏi cùng câu thì "Không tìm thấy", nội dung văn bản hạn chế không ai hỏi ra, sổ không có chữ nào của câu hỏi.make do-hoi-dap(bước CI): recall@3, tỉ lệ từ chối đúng, p50 / p95; thoát 1 khi có nguồn bị cấm hoặc recall@3 dưới 80 %.make smoke+3 (tổng 66); e2ehoi-dap.spec.ts. Test: CSDL +31, svc-files +436, giao diện +15, scripts +18. - Tác tử gợi ý giao việc (F4.3): vào sổ một văn bản đến thì quy trình
van-ban-dentách hai nhánh song song — bước chỉ đạo như cũ (lãnh đạo có việc ngay) và việc giao ra ngoàigoiYGiaoViec(topicmoc-goi-y, timer ngắt 10 phút; không nối lại, nên svc-files hay tác tử lỗi không chặn văn bản giây nào). Tác tửmoc-assignercủa svc-workflow (advisor.py) lấy các văn bản đến đã giao giống văn bản này qua route nội bộPOST /internal/similar-documentscủa svc-files (chỉ token dịch vụ, không có trong OpenAPI, không có route APISIX; câu tìm từ trích yếu; dùng lại truy xuất và hậu kiểm F4.2, tìm theo từ khóa luôn kèm lượt tìm nới để từ hiếm như "II" của "học kỳ II" không quyết văn bản nào được xét; có mô hình nhúng thì tìm lai, tối đa 3 giây), rồi bỏ phiếu kNN có trọng số, không cần mô hình (suggest.py): đơn vị chủ trì (chỉ phòng có lãnh đạo phòng), độ tin cậy cao / vừa / thấp, hạn = trung vị số ngày của các căn cứ (đẩy qua thứ Bảy, Chủ nhật; văn bản đã ghi hạn thì không gợi ý hạn), tối đa 5 căn cứ. Không đủ văn bản tương tự thì không gợi ý; svc-files không trả lời thì ghifailed, không thử lại. BiếnMOC_SUGGEST_ENABLED,MOC_SUGGEST_POLL_INTERVAL_S,MOC_SUGGEST_TIMEOUT_S,MOC_SUGGEST_MIN_SCORE,MOC_ASSIGNER_ID. Khác kế hoạch: nhánh song song từ lúc vào sổ thay cho "ngay trước bước chỉ đạo" (QĐ-005 trongdocs/architecture.md). - Khối "Gợi ý giao việc" ở đầu form chỉ đạo của lãnh đạo đơn vị: đơn vị chủ trì, độ tin cậy bằng chữ, hạn đề xuất ("n ngày, như các văn bản tương tự"), tối đa 3 căn cứ "số ký hiệu — trích yếu → đơn vị · số ngày" đọc bằng token người xem (chỉ văn bản người xem đọc được). "Dùng gợi ý" chỉ điền form, lãnh đạo sửa nếu muốn rồi tự bấm giao; chip "Theo gợi ý" / "Khác gợi ý". Văn bản hạn chế không hiện khối.
- Gợi ý lưu ở bảng
office.assignment_suggestions(migration1792281602100, một dòng mỗi văn bản; căn cứ chỉ là id, điểm, đơn vị, số ngày) cùng một dòng sổassignment.suggestedcủamoc-assigner(vai trò Hasuraagent) trong một mutation — tự hiện ở tab Lịch sử, không có trích yếu; chỉ lãnh đạo đơn vị đọc gợi ý. Viewoffice.v_assignment_suggestion_accuracy: tỉ lệ lãnh đạo giữ đơn vị, giữ hạn, sai số hạn trên dữ liệu thật. make demo-giao-viec(10 bước, cũng là bước CI; lịch sử giao việc hư cấuseed/giao-viec/lich-su.json: 29 văn bản của Tổ Đào tạo và Bộ môn KTPM, một mồi hạn chế) — kể cả dừng svc-files: văn bản vẫn tới bước chỉ đạo ngay, gợi ý ghifailed.make do-giao-viec(bước CI): leave-one-out top-1/2/3, độ phủ, sai số hạn, p50 / p95; thoát 1 khi căn cứ là văn bản hạn chế hoặc top-1 dưới 60 %. Trên dữ liệu thử: top-1 29/29 (gốc "luôn đoán đơn vị đông nhất" 55 %; dữ liệu chia cụm gọn và chỉ 2 đơn vị nên cao hơn thực tế), sai số hạn trung bình 1,1 ngày, một lần gợi ý p50 71 ms.make smoke+5 (tổng 71); e2egiao-viec.spec.ts. Test: CSDL +47, svc-files +54, svc-workflow +93, giao diện +13, scripts +24.- Kho văn bản đến mẫu của F4.1 (
seed/van-ban-den/) lên 22 văn bản (15 PDF có lớp chữ, 7 ảnh / bản scan) để đo đúng "Hoàn thành khi" của Phase 4 (trên 20 công văn mẫu). 8 mẫu mới thêm ở cuối, khó hơn 14 mẫu đầu: hướng dẫn hai trang, nghị quyết HĐND, thông báo kết luận, kế hoạch liên tịch, công văn bệnh viện; ký hiệu ba đoạn, có năm, số có chữ cái phụ; ngày viết không theo NĐ 30 ("ngày 5 tháng 03",12/10/2026); ảnh chụp điện thoại nghiêng 3,2° trên mặt bàn, bản scan nền xám, PDF scan 3 trang. Nội dung 14 mẫu cũ giữ nguyên; cả 22 tệp in bằngmake van-ban-mautrong image svc-files nên chạy lại ra đúng từng byte (10 PDF cũ khác byte bản trước vì bản phông nhúng; mẫu 04 ngắt một dòng khác, chữ không đổi). Ngưỡng củamake do-doc-van-banvàtest_docread_samples.pyđổi sang tỉ lệ cùng độ nghiêm (≥ 90 % / ≥ 80 %, không ôhighnào sai);make do-doc-van-bancòn thoát 1 khi cả kho dưới 20 văn bản hay số ký hiệu, ngày ban hành đúng không quá nửa; ngưỡng TSV chốt cho cả 7 bản scan. Số đo trên 22 văn bản: số ký hiệu đúng 20/22, ngày ban hành đúng 22/22 (bảng đủ trong README svc-files). - Ký số văn bản đi (F4.5, chỉ minh họa — không có giá trị pháp lý như chữ ký số công cộng):
/internal/issueký bản phát hành không hạn chế theo PAdES B-B (chữ ký chứng nhận không cho sửa, ô ký ẩn) trước khi băm, nêncontent_sha256, sổ kiểm toán, bản liên thông và cổng xác thực đều theo bản đã ký. Khóa RSA-3072 của từng đơn vị (moc-ky-so-<đơn vị>) sinh và nằm trong Transit của két,svc-fileschỉ nhờ két ký; chứng thư 1 năm do CA nội bộ một tầng trong két cấp (openbao/start.shdựng mountpki, CAmoc-root, rolemoc-van-thukhi khởi tạo két mới). Tự kiểm bằng pyHanko trước khi lưu; tệp và dòngcore.file_signaturesghi trong một mutation. Két chưa có PKI → ký số tự tắt (nhật kýoffice.signing.disabled, dò lại sau 5 phút); ký lỗi → phát hành không ký (office.document.sign_failed); két không trả lời →503 signing_unavailable. Văn bản hạn chế không bao giờ ký. Tắt bằngMOC_SIGN_ENABLED=false. - Migration
1792281602200_file_signatures: bảngcore.file_signatures(chuẩn, giờ ký tự khai, người ký, CA, số sê-ri, vân tay chứng thư, khóa và phiên bản Transit; sổ kiểm toán),svc-fileschỉ thêm; khung nhìn cổng xác thựcoffice.issued_documentsthêm năm cột chữ ký, tìm theo băm nên văn bản đã nộp lưu vẫn hiện chữ ký; chỉ mụccore.files (tenant_id, sha256)./api/verify*trả thêmdocument.signature(illustrative: true). - Cổng xác thực
/xac-thuc/<mã>có khối "Chữ ký số (minh họa)": chuẩn, người ký, CA, số sê-ri, giờ ký, chú thích luôn hiện "chỉ để minh họa — không có giá trị pháp lý…"; văn bản không ký (trước F4.5, ký số tắt hay ký lỗi) hay văn bản hạn chế ghi "không có chữ ký số, tính nguyên vẹn vẫn kiểm bằng SHA-256". python -m svc_files.signing verify [--ca TỆP] < tệp.pdf(trong container svc-files): kiểm bằng pyHanko, chỉ tin CA nội bộ, không tải gì từ mạng; dòng đầu nói kết quả (hợp lệ / đã sửa sau khi ký / không khớp nội dung / CA lạ / không có chữ ký / không đọc được), mã thoát 0/1/2.- Màn demo
make demo-ky-so(8 bước, cũng là bước CI): CA trong két → phát hành →/ByteRangephủ cả tệp, cổng xác thực có chữ ký, CSDL cùng số sê-ri → CLI hợp lệ → thêm một lớp sửa ghi nối: "đã bị sửa sau khi ký", băm cả tệp không khớp → lật một bit: "không khớp nội dung" → văn bản hạn chế phát hành không chữ ký → văn bản cũ xác thực như trước.make smoke+5 kiểm (bỏ qua, in lý do, khi két chưa có PKI hay tắt ký số); e2evan-ban-di.spec.tskiểm khối chữ ký. QĐ-007 trongdocs/architecture.md, mục ký số ở README, README svc-files,docs/mo-hinh-du-lieu.md,docs/demo-script.md. Phụ thuộc mới của svc-files: pyHanko 0.37.0, pyhanko-certvalidator 0.32.1 và 15 gói kéo theo, ghim kèm băm (DEPENDENCIES.md). Test: CSDL +28, svc-files +28, scripts +22, giao diện +5. - Che thông tin cá nhân (F4.4): bộ nhận dạng
pii.pytự viết, chỉ dùngrecủa thư viện chuẩn — không Presidio, không thêm phụ thuộc (DEPENDENCIES.mdxếp Presidio, phonenumbers vào "đã cân nhắc nhưng không dùng"). Nhận theo cấu trúc, không theo "dãy số đủ dài": số định danh / CCCD 12 số (mã tỉnh, chữ số thế kỷ, năm sinh), số điện thoại (đầu số di động đã cấp, máy bàn02x,+84, mã vùng(024), viết liền hay theo nhóm), email; CMND và số tài khoản chỉ khi đứng sau từ khóa. Nhóm số cách nhau một ký tự trắng bất kỳ, nên số bị dàn trang ngắt dòng trong chữ PDF, OCR ("0912 345⏎678") vẫn bị che; nhóm đầu ≥ 3 chữ số, nên dãy số nhỏ của bảng, lịch ("Tuần 01 02 03 04 05 06") không bị che; bộ che lặp tới điểm bất động (idempotent). Không che UUID, số ký hiệu, ngày, tiền, tổng đài 1800 / 1900, mã số thuế sau "MST". Dòng lệnhpython -m svc_files.pii mask|count(countkhông in giá trị);svc_workflow/pii.pylà bản chép y hệt byte, test đỏ khi hai bản lệch. - Mô hình chạy tại chỗ chỉ nhận bản đã che (F4.4): mọi lời gọi Ollama — nhúng đoạn, câu hỏi, trích yếu (F4.2, F4.3), viết câu trả lời (F4.2), viết trích yếu (F4.1, che trước khi cắt
MOC_AI_MAX_CHARS) — qua bộ che ở lối ra;chatcòn phải che thêm thì cảnh báooffice.assist.pii_at_egress. Ngữ cảnh F4.2 dùng nhãn đánh số theo lượt ([SĐT-1], cùng số cùng nhãn ở câu hỏi và đoạn), che rồi mới cắt cửa sổ 700 ký tự nên không lọt mẩu số, trích dẫn[n]và trang không lệch. Chỉ mục tìm kiếm, câu trích nguyên văn, nguồn trả người hỏi và sổ kiểm toán giữ số thật. Trích yếu mô hình viết có nhãn che bị loại (model_rejected).office.assist.asked,office.assist.model_rejected,office.intake.readthêm khóamasked(số chỗ đã che, không có giá trị). Kho mẫu không có chỗ nào bị che, nên số đomake do-hoi-dapkhông đổi. - Nhật ký không mang thông tin cá nhân (F4.4): svc-files, svc-workflow gắn
PiiFiltervào logger dịch vụ và uvicorn (uvicorn.error,uvicorn.access), che cả traceback; che hỏng thì cả dòng thànhcore.log.redaction_failed. Tệp bị chặn têndon-0912345678.txtghidon-[SĐT].txtở nhật ký, sổ kiểm toán giữ tên gốc. Nhật ký truy cập của uvicorn, APISIX (gateway/config.yaml) và nginx của app-office bỏ chuỗi truy vấn (?q=…,?code=…); APISIX, nginx bỏ cả Referer, và giao diện gửiReferrer-Policy: strict-origin(trước làsame-origin) nên Referer chỉ còn gốc tên miền. Bỏ hẳn chứ không che bằng regex: chuỗi truy vấn mã hóa phần trăm thì bộ che không nhận ra (gateway/README.md). - Trang Hỏi đáp: câu trả lời mô hình viết có nhãn che (
[SĐT-1]…) thì ghi rõ số thật nằm trong văn bản nguồn (câu trích nguyên văn, các nguồn bên dưới) và tự mở khối câu trích (F4.4); mẫu nhãn của giao diện trùngLABEL_REcủapii.py(test so hai mẫu). - Màn demo
make demo-che-pii(F4.4, 7 bước, cũng là bước CI): văn thư nhận lá đơn hư cấu có CCCD, số di động, máy bàn, email, số tài khoản → tìm theo số CCCD ra đúng đơn, trợ lý trích nguyên văn có số thật → tệp EICAR tên có số điện thoại bị chặn → nhật kýapisix,app-office,svc-files,svc-workflowkhông còn giá trị nào ở mọi cách viết, dòng truy cập không còn?q→ bật mô hình thìoffice.assist.askedcómasked≥ 2, không bật thì in bản che.make do-che-pii(không cần Docker, đã chạy trongmake test): che nhầm trên kho mẫu và 15 đột biến mã củapii.py. Đo 22/10/2026: 22 văn bản đến (15 PDF có lớp chữ, 7 bản scan), 6 văn bản hỏi đáp, 1 248 chuỗi nhãn, câu hỏi, lịch sử giao việc — 81 023 ký tự, 747 chuỗi số ứng viên, 0 chỗ bị che; 15/15 đột biến bị giết. QĐ-006 trongdocs/architecture.md, mục che thông tin cá nhân ở README svc-files,docs/demo-script.md,gateway/README.md. Test: svc-files +62, svc-workflow +6, scripts +19, giao diện +3. - Tài liệu triển khai lên máy chủ
docs/trien-khai.md(F1.9): Caddy nhận HTTPS (Let's Encrypt) trước APISIX chỉ nghe127.0.0.1, người vận hànhmoc, tường lửa, cài ra/opt/moc, lệnh kiểm sau khi lên, sao lưu hằng đêm, nâng cấp. Biến.envmớiMOC_TRUSTED_PROXY(mặc định127.0.0.1/32, không tin ai): APISIX giữX-Forwarded-Proto,-Host,-Portvà lấy IP người dùng từX-Forwarded-Forchỉ với yêu cầu từ dải này (gateway/README.md). Diễn tập 22/10/2026 trên máy dev: proxy TLS ở cổng 443 trước bản cài, smoke, e2e, màn demo quahttps://.
Đã sửa đổi
- Quy trình Git chuyển sang Git Flow (
docs/git_flow.md):developlà nhánh mặc định, mọi pull request vào đó;mainchỉ nhận PR phát hànhchore(release): vX.Y.Ztừdevelophoặchotfix/X.Y.Z. Workflow mới PR (scripts/check_pr.py) kiểm nhánh nguồn, tiêu đề theo Conventional Commits với loại và phạm vi trong danh sách, và với PR vàomainthì CHANGELOG có mục phiên bản có dòng thay đổi, thẻ chưa có; CI chạy cho cảdevelopvàmain, hủy lượt cũ trên mọi nhánh trừmain; workflow Release dừng khi thẻ không nằm trênmain. Ruleset khai trong.github/rulesets/(PR, 1 duyệt, 8 job xanh; thẻv*không sửa, không xóa được). Mục 11 củagit_flow.mdghi cách làm thực tế trước và sau 23/10/2026. Test scripts +21. - Đổi tên hiển thị Mộc → Signet (nhẫn ấn đóng dấu lên văn bản, giữ ý "đóng dấu" của tên cũ): giao diện (
<title>, thanh bên, trang đăng nhập, cổng xác thực, biểu tượng con dấu thay hình cây), realm Keycloak (displayName, tên client), tiêu đề API, chuỗi in ra của CLI và script, bảng điều khiển Grafana "Signet — cổng API", PDF phát hành và sổ in, mẫu issue, tài liệu, header giấy phépSignet Team. Tên mã kỹ thuậtmocgiữ nguyên: biếnMOC_*, realmmoc, client id, project compose, image,scripts/moc.py,/opt/moc, tiêu đềX-Moc-*, góimoc-X.Y.Z.tar.gz. Bản cài cũ: Keycloak chỉ nạp realm ở lần khởi động đầu nên trang đăng nhập Keycloak còn tên Mộc tới khi đổi Display name của realm trong trang quản trị; két đã dựng PKI giữ CA tên cũ (chữ ký vẫn kiểm được), két mới có "CA nội bộ Signet - MẪU". Văn bản mẫu đã commit (kho văn bản đến, kho hỏi đáp) giữ chân trang "chạy thử Mộc" — đổi là phải in lại PDF, đo lại. - README viết lại gọn (bài toán, bốn con dấu của lõi, tính năng, bằng chứng bốn nguyên lý, kiến trúc, cài đặt, kiểm thử); sáu mục vận hành (thêm đơn vị, sao lưu, két bí mật, GitOps, giám sát, IT/OT) chuyển nguyên văn sang
docs/van-hanh.md, các chỗ trỏ "README mục …" trỏ theo. - Văn thư không còn ghi thẳng bảng văn bản (
insert_office_documents_one,update_office_documents) lẫn bảng tệp đính kèm (insert_office_document_files_one,delete_office_document_files): văn bản đến kèm tệp vào qua ActionreceiveIncoming(data: jsonb!), nên không đi vòng được lớp kiểm của svc-workflow. Sửa văn bản đến chưa vào sổ không còn qua GraphQL (giao diện chưa từng dùng). Migration dọn văn bản đến đã có (khoảng trắng thừa, ký tự ẩn, dạng Unicode tổ hợp) trước khi thêm ràng buộc; dòng nào còn sai thì cảnh báo kèm mã văn bản. Sổ kiểm toán ghi lần tiếp nhận làsvc-workflowlàm thay văn thư, kèm đúng người bấm. Ngày "hôm nay" của giao diện tính theo giờ Việt Nam (#34). - Hasura chờ
completeTasktới 90 giây (trước là 30): phát hành gồm cấp số, dựng PDF, quét, lưu kho, máy chậm có thể quá 30 giây. Cổng APISIX chờ Hasura 100 giây (mặc định 60). make seedtạo Organization cho hai đơn vị mẫu (cntt,kt) và đưa tài khoản mẫu vào (cùng hàm vớimoc tenant create, chạy lại thì giữ nguyên); realm bậtorganizationsEnabled— bản đã dựng thìmake seedtự bật, không cầnmake reset.- Tiếp nhận văn bản đến (F4.1): giao diện gọi tác tử đọc khi tệp quét sạch;
make down,make resetdừng cả profileai,ai-pull;.gitignoretheo dõiseed/van-ban-den/*.pdf;scripts/license-header.shcoi*.tsvlà dữ liệu. - Trợ lý hỏi đáp (F4.2):
make ai-modelkéo cả mô hình nhúng (MOC_AI_EMBED_MODEL, mặc địnhqwen3-embedding:0.6b, khoảng 640 MB) vàMOC_ASK_MODELkhi đặt riêng. Hàng đợi đọc chữ (F2.7) đọc thêm bản phát hành của văn bản đi; migration1792281602000xóa các dòng chữ đã đọc để đọc lại một lần, lần này có ranh giới trang — ngay sau khi nâng cấp, tìm theo nội dung có thể thiếu trong vài phút. - Gợi ý giao việc (F4.3): "Báo cáo xong" cập nhật văn bản trước giao việc, cùng thứ tự khóa với tác tử, nên hai bên không chờ vòng nhau. Test BPMN của nhắc hạn chỉ xét timer gắn vào việc của người, tìm worker nhắc hạn theo topic
moc-sla. - Trợ lý hỏi đáp (F4.2): tìm theo từ khóa luôn kèm lượt tìm nới (không chỉ khi chưa đủ đoạn), rồi xếp chung mọi đoạn theo tỷ lệ trọng số câu hỏi mà đoạn có. Trước đây đoạn của lượt chính luôn đứng trước: hỏi "Điều kiện xét học bổng khuyến khích học tập là gì?" khi kho có vài công văn "đủ điều kiện xét tốt nghiệp" thì ba nguồn đầu đều lạc đề, công văn trả lời đứng thứ tư. Từ phổ biến (kết quả mỗi từ chỉ lấy một số đoạn đầu) vẫn được tính cho đoạn có nguyên văn từ đó; lượt nới lỗi thì giữ kết quả lượt chính.
Đã sửa lỗi
-
Ô chữ đại diện ở đầu trang lấy chữ đầu của từ cuối trong họ tên và không viết hoa: "Tài khoản Giám khảo" ra "k". Nay lấy chữ đầu của tên (claim
family_name— firstName của Keycloak là họ và đệm), viết hoa: G. README ghi thiếu tổ trưởng, trưởng bộ môn trong các vai trò bắt buộc TOTP; lời nhắc cuốiseed.pythêm lệnh của bản cài (python3 scripts/totp.py—/opt/mockhông có Makefile). -
Sau Cloudflare Tunnel,
seed.pyvàsmoke.pydừng với "APISIX chưa sẵn sàng" dù trang vẫn mở được: Browser Integrity Check của Cloudflare trả 403 cho User-Agent mặc địnhPython-urllib. Các kịch bản giờ gửiUser-Agent: Signet-scripts. -
Trang văn bản: thông báo realtime đến đúng lúc bấm thao tác (vd lời nhắc quá hạn khi văn thư phát hành) làm hai lượt tải chồng nhau, lượt cũ về sau đè lượt mới — văn bản vừa phát hành hiện lại "Đã duyệt", thẻ liên thông thôi hỏi lại trạng thái. Lượt tải cũ giờ bỏ kết quả (như trang văn bản đến); hộp việc cũng vậy.
-
Bấm lại khi hoàn thành việc lâu (phát hành) hay hai lần gọi chen nhau không còn ghi hai lần (hai phân công, hai lần cấp số và dựng PDF): svc-workflow cho các lần gọi cùng một việc chạy lần lượt, lần sau thấy việc đã xong; CSDL có chỉ mục duy nhất
document_events_task_oncetheotask_idcủa sự kiện, nên lần ghi thứ hai (ví dụ từ bản sao dịch vụ khác) bị chặn cả giao dịch và svc-workflow chỉ hoàn thành nốt việc trong Flowable. Sự kiện ghi kèm biến gửi Flowable (data.flow): bấm lại sau lỗi "Đã lưu, nhưng chưa chuyển được bước tiếp" đi đúng nhánh đã ghi, kể cả khi lần bấm lại chọn khác, và việc xử lý văn bản đến không còn kẹt ở lần bấm lại. Dữ liệu cũ đã trùng thì migration cảnh báo và chỉ chặn từ sau dòng trùng cuối. -
Vào sổ, trình văn bản đi bấm đúp, hay trùng lúc với job đối soát, không còn khởi chạy hai quy trình cho một văn bản (mỗi bước hai việc, việc này đè trạng thái của việc kia): quy trình chỉ được gắn khi văn bản chưa có (
update … where process_id is null); lần gọi thua theo quy trình đã gắn và hủy quy trình nó vừa khởi chạy (hủy không được thì nhật ký ghioffice.process.duplicate_left). -
Quản trị đơn vị mở app gặp lỗi (hộp việc chỉ dành cho người xử lý văn bản): trang đầu của vai trò này là Sổ kiểm toán, và sổ không còn nút "Mở văn bản" (quản trị đơn vị không đọc nội dung nghiệp vụ). Trang văn bản tải lỗi thì hiện lỗi kèm nút thử lại thay vì trang trống.
-
Ngày giờ trên giao diện luôn theo giờ Việt Nam, kể cả trên máy đặt múi giờ khác; ngày không kèm giờ (ngày ban hành, hạn) không còn lệch một ngày ở múi giờ âm.
-
Danh sách văn bản đến, văn bản đi không còn cắt ở 200 dòng (văn bản cũ, dễ quá hạn, bị ẩn): mỗi lần 50 dòng, nút "Xem thêm". Có văn bản mới vào sổ hay đổi trạng thái giữa hai lần bấm thì không lặp, không sót dòng; "Xem thêm" lỗi thì báo ở góc màn hình, bảng đang xem vẫn còn.
-
Lưu dự thảo không còn sửa được (đã trình, đã đổi trạng thái) không còn báo "Đã lưu" khi CSDL không ghi gì.
-
Hết phiên đăng nhập giữa lúc soạn dự thảo hay tiếp nhận văn bản: phần đang nhập được giữ trong phiên của trình duyệt (sessionStorage) và khôi phục khi quay lại; văn bản hạn chế không được giữ. Tự rời trang (chuyển sang trang khác của ứng dụng) hay đăng xuất thì bỏ phần đang nhập, mở lại trang soạn là bắt đầu mới.
-
e2e thông báo tìm văn bản trong đúng hộp việc, không khớp nhầm thông báo nổi; test tích hợp tìm kiếm của svc-files gắn tệp mã hóa vào văn bản hạn chế như ràng buộc của F3.7.
-
Văn bản hạn chế giao cho người không đọc được: lãnh đạo nhận việc chỉ đạo, tổ trưởng nhận phân công mở văn bản ra "không tìm thấy". Nay người phải làm việc đang mở được thêm vào danh sách được xem;
completeTasktừ chối (403restricted) người không đọc được văn bản thay vì cho làm mù (#47). -
svc-workflow gọi svc-files (phát hành, gửi liên thông, tìm văn bản tương tự) không còn hỏng "token không hợp lệ" sau khi máy chủ ngủ hay VM bị tạm dừng: token dịch vụ giữ trong bộ nhớ tính hạn theo đồng hồ monotonic, đồng hồ này đứng yên lúc máy ngủ nên token đã hết hạn vẫn được dùng. Nay tính theo giờ thật như
expcủa token; svc-files vẫn trả 401 thì bỏ token, lấy token mới và gửi lại đúng một lần (nhiều lời gọi cùng lúc chỉ hỏi Keycloak một lần). -
Đặt sau proxy TLS (máy chủ có HTTPS) thì không đăng nhập được: APISIX 3.18 thay
X-Forwarded-Protocủa proxy bằnghttp, Keycloak trảtoken_endpointdạnghttp://…, trình duyệt chặn (nội dung hỗn hợp, CSPconnect-src 'self'); mọi người dùng chung một bộ đếm giới hạn tốc độ. Nay khaiMOC_TRUSTED_PROXY(xem phần Đã thêm). -
make demo-che-pii,make demo-hoi-daptrên bản cài mới: bước đợi chỉ mục dừng ngay khi văn bản có trong nguồn qua trích yếu (vào chỉ mục lúc tiếp nhận), lúc chữ các trang chưa được đọc, nên câu trả lời thiếu đoạn cần trích và màn đỏ. Nay đợi tới khi một trang của văn bản có trong nguồn (test scripts +4). -
Kịch bản dòng lệnh (smoke, demo) đỏ "Invalid user credentials" khi mã TOTP của khoảng 30 giây vừa được dùng ở nơi khác (đăng nhập trên trình duyệt, e2e chạy từ thư mục mã nguồn trong khi kịch bản chạy từ bản cài — hai nơi không chung sổ mã): Keycloak 26 trả 400 cho mã đã dùng y như sai mật khẩu, còn bước thử mã kế tiếp chỉ chờ 401 nên không bao giờ chạy. Nay thử tối đa bốn mã liền nhau, cách nhau 1,5 giây — ba mã đang hiệu lực đều đã bị dùng thì đợi sang khoảng 30 giây mới (test scripts +4).
-
Soạn văn bản đi: đang sửa một dự thảo mà bấm "Soạn văn bản đi" ở thanh bên (cùng trang, chỉ bỏ
?id=) thì form còn nội dung, tệp, cờ hạn chế của dự thảo trước và Lưu tạo một văn bản mới chép từ nó. Nay đổi?id=là dựng lại trang (e2evan-ban-di.spec.tskiểm); dự thảo hạn chế chỉ cho chọn tệp PDF ngay ở ô chọn tệp. -
Trình văn bản đi (
submitOutgoing): lần trình đầu lỗi sau khi quy trình đã khởi chạy, trình lại với người ký khác thì việc ký vẫn giao cho người chọn lần trước — nay ghi lại người ký vào quy trình trước khi ghi CSDL; quy trình đã qua bước duyệt cấp phòng thì từ chối (409signer_locked). Trả lại đã ghi mà Flowable chưa sang bước sửa, người soạn trình lại ngay thì CSDL là "đang duyệt" còn quy trình quay về bước sửa, người soạn kẹt — nay trả 409return_pending, không ghi gì. Test svc-workflow +7. -
Nộp lưu hồ sơ (F3.8) không còn làm tụt số liệu dashboard điều hành (F3.11) và số đo đọc văn bản đến (F4.1): hàm nộp lưu gỡ dòng thời gian, liên kết tệp mà hai khung nhìn tính từ đó, nên văn bản đã nộp lưu rơi khỏi số văn bản, số giờ, số đúng hạn của các tháng cũ. Nay lúc xong và tệp bản chính giữ ở
office.archived_document_factstrong cùng giao dịch; văn bản nộp lưu trước bản sửa được điền lại từ sổ kiểm toán khi chạy migration. Test CSDL +1. -
Ghi chú trang Release lấy từ CHANGELOG: mục cuối kéo theo khối mẫu trong chú thích HTML (trang v0.1.0 đã đăng có các dòng
### Đã thêm … -->thừa), thiếu mục thì vẫn đăng Release với một câu thay thế. Nayscripts/release-notes.sh(dùng ởrelease.shvà workflow Release) bỏ qua chú thích HTML; thiếu mục, hay mục chỉ có tiêu đề nhóm, thìrelease.shkhông gắn thẻ và workflow dừng. Test scripts +5.
Bảo mật
-
Mọi image bên thứ ba (
docker-compose.yml, cácDockerfile, image Playwright củamake e2e) ghim thêm digest (thẻ@sha256:…, index đa nền tảng đã chạy CI): thẻ bị đẩy lại không đổi được bản máy chủ kéo về. Ba thẻ đã bị đẩy lại từ lúc đội kéo (postgres:16-alpine,python:3.13.16-slim,clamav/clamav:1.5.4) — nay vẫn chạy đúng bản đã kiểm. Cách nâng phiên bản:DEPENDENCIES.mdmục Cách đọc. -
GitOps chỉ áp commit đã ký khi đặt
MOC_GITOPS_ALLOWED_SIGNERS(tệp allowed_signers, khóa SSH của người được đẩy nhánhdeploy): reconcilergit verify-committrước khi dừng Mộc; chưa ký, khóa lạ, chữ ký OpenPGP/X.509 (kể cả khi keyring của người dùngmoctin khóa đó) thì không áp, Mộc chạy tiếp bản cũ. Để trống thì như trước (docs/van-hanh.mdmục GitOps;docs/git_flow.mdmục 7: đẩy nhánh bằnggit merge --no-ff -S). Test scripts +4. -
Không phát hành lại một phiên bản:
release.shdừng trước khi gắn thẻ khi thẻvX.Y.Zđã có trênorigin(không hỏi đượcorigincũng dừng) hay chỉ có ở máy (gợi ý gỡ thẻ của lần chạy dở); workflow Release dừng khi thẻ đã có Release, kể cả bản nháp mà GitHub để lại khi xóa thẻ của Release đã đăng; action không ghi đè tệp cùng tên, thiếu tệp thì lỗi, và một bước sau báo lỗi khi không tải lên đủ hai tệp. Gói.tar.gz,.sha256người dùng đã tải về không bị thay dưới cùng tên. Test scripts +14 (chạy lệnh của bước workflow vớighgiả). -
Ký số (F4.5): khóa ký của từng đơn vị không ra khỏi két (policy
svc-fileschỉ cótransit/keys|sign/moc-ky-so-*vàpki/sign/moc-van-thu, khôngexport,backup). Ai chiếm đượcsvc-filesvẫn ký được, xoay được khóa ký và xin được chứng thư cho khóa tự sinh, nhưng không mang khóa riêng đi được — cùng mức tin như khóa dữ liệumoc-files(docs/van-hanh.mdmục Két bí mật). Két khởi tạo trước F4.5 không ký số cho tới khi quản trị nâng cấp tay (cùng mục). -
Che thông tin cá nhân (F4.4): sổ kiểm toán cố ý giữ bản gốc (có phân quyền và chuỗi băm, che vào là sửa chứng cứ), số chỗ đã che chỉ ghi ở nhật ký. Chưa che: họ tên, địa chỉ, số dính chữ (
ĐT0912…), CMND và số tài khoản không có từ khóa; nhật ký Hasura, PostgreSQL, Keycloak, Meilisearch vàerror_logcủa APISIX, nginx (vẫn có?q=…khi một plugin ghi lỗi) không qua bộ che; véc-tơ nhúng tính trước F4.4 còn tới khi nhúng lại (README svc-files). -
Văn bản hạn chế (F3.7) không bao giờ có gợi ý đọc tự động (F4.1): svc-files trả
409trước khi đọc byte nào — tệp mã hóa, hay tệp rõ còn gắn vào văn bản hạn chế (dữ liệu có từ trước F3.7) — nên không tới OCR, không tới mô hình; triggerintake_suggestions_guardchặn thêm ở CSDL việc lưu gợi ý của các tệp đó, xóa gợi ý khi văn bản bật hạn chế. Vai tròagentkhông đọc, ghi văn bản; dòng sổ và nhật ký của tác tử không mang nội dung văn bản. -
Trợ lý hỏi đáp (F4.2) chỉ dựa trên văn bản người hỏi được xem: lọc trước theo vai trò ngay trong chỉ mục (viết như
office.document_visible), rồi hậu kiểm bằng Hasura với token người hỏi — cả hai trước mô hình và trước người dùng, nên chỉ mục còn cũ cũng không làm lộ; văn bản hạn chế không bao giờ vào chỉ mục, bật hạn chế là xóa mọi đoạn. Chữ văn bản đưa cho mô hình nằm trong thẻ riêng, thẻ giả trong văn bản bị bỏ. Dòng sổ và nhật ký của trợ lý không mang câu hỏi, câu trả lời hay đoạn trích. -
Văn bản hạn chế không bao giờ có gợi ý giao việc và không bao giờ làm căn cứ (F4.3): svc-files từ chối văn bản đích hạn chế trước khi tìm, chỉ mục không có đoạn của văn bản hạn chế, hậu kiểm bỏ văn bản vừa bật hạn chế; trigger
assignment_suggestions_guardkhóa văn bản đích và các căn cứ rồi chặn ở CSDL, bật hạn chế sau đó thì xóa gợi ý của văn bản và gỡ nó khỏi căn cứ của gợi ý khác. Dòng sổ, nhật ký của tác tử và lệnh đo không mang trích yếu hay chữ nào của văn bản. -
Action GitHub trong CI và quy trình phát hành ghim theo SHA commit, image công cụ CI ghim theo digest; workflow CI chỉ có quyền đọc kho.
-
Xác thực hai bước TOTP (F1.1): đăng nhập trình duyệt bắt buộc mã với văn thư, lãnh đạo phòng, lãnh đạo đơn vị, quản trị đơn vị (chưa cài thì Keycloak bắt cài ở lần đăng nhập đầu); vai trò khác chỉ hỏi mã nếu đã tự cài.
make seedcài sẵn TOTP cho tài khoản mẫu,make totpin mã;make smokethêm 2 kiểm tra, e2e đăng nhập kèm mã. -
Client
moc-cli(lấy token bằng mật khẩu cho kịch bản dòng lệnh) hỏi mã TOTP theo đúng quy tắc của trình duyệt, tham sốotp, thay vì bỏ qua bước này — không còn là đường vòng qua xác thực hai bước. Kịch bản trongscripts/tự tính mã từMOC_DEMO_TOTP_SECRET; kịch bản và e2e ghi chung một sổ mã đã dùng (tests/e2e/.otp-used.json, theo địa chỉ hệ thống) nên không gửi lại mã đã dùng — Keycloak đếm mỗi mã bị từ chối là một lần đăng nhập sai, đủ 10 lần thì khóa tài khoản; mã vẫn bị từ chối thì đợi quá 1 giây mới thử mã kế.make smokethêm 1 kiểm tra (tổng 58). Realm chỉ nạp ở lần khởi động đầu: stack đã dựng phảimake reset(xóa dữ liệu) hoặc sửa tay luồngmoc-cli-direct-granttrong trang quản trị Keycloak (auth/README.md). -
ClamAV báo tệp có mật khẩu (zip, 7z, rar, PDF phải nhập mật khẩu mới mở) và tệp giải nén vượt giới hạn quét (bom nén, lồng quá sâu) thay vì coi là sạch (
AlertEncrypted,AlertExceedsMax): svc-files chặn (422unscannable, kèm cách sửa), ghifile.rejectedvào sổ kiểm toán với nhãn "Chặn tệp không quét được". PDF chỉ khóa quyền sửa, in (mở không cần mật khẩu) vẫn nhận. -
Thông báo về văn bản hạn chế không còn mang số ký hiệu, trích yếu, cơ quan ban hành: svc-workflow chỉ gửi nhãn, trigger CSDL gọt nội dung cho mọi đường ghi và gọt cả thông báo đã gửi khi văn bản chuyển sang hạn chế. Tóm tắt dòng sổ kiểm toán (
overview) về văn bản hạn chế mà người xem sổ không đọc được chỉ còn tên trường, không còn giá trị (hàmoffice.audit_overview_seen) (#47). -
Pillow 12.3.0 → 13.0.0, pypdf 6.19.0 → 6.21.1 (
svc-files): vá các lỗ hổng OSV-Scanner báo trong CI (Pillow các GHSA đọc ảnh, pypdf 5 GHSA) — cả hai đọc tệp người dùng tải lên; certifi 2026.7.22 → 2026.10.19 (bỏ gốc emSign có CA trung gian lộ khóa). Ngoại lệ quy tắc hai tuần, ghi ởDEPENDENCIES.md. -
Giới hạn đăng nhập theo IP (F1.2) không còn né được bằng cách viết khác đường (
…/token/,…/token;x): mọi POST vào realm qua routedang-nhap-post, chung bộ đếm với route đăng nhập. Cổng gỡ thêmX-Hasura-Access-Keytừ phía ngoài (tên cũ của khóa quản trị; Hasura 2.51 đã không nhận — kiểm 22/10 — gỡ để phòng xa). -
Realm
mocđặt luồng direct grant mặc định làmoc-cli-direct-grant: client có sẵnadmin-cli(lấy token bằng mật khẩu) cũng phải gửi mã TOTP, không còn là đường vòng qua xác thực hai bước. Stack đã dựng:make resethoặc đặt tay (auth/README.md).make smoke+3 (tổng 79): đường token viết khác vẫn bị đếm,admin-clikhông có mã bị từ chối, khóa quản trị Hasura từ ngoài (cả tên cũ) không có tác dụng. -
Văn bản hạn chế (F3.7): PDF người dùng tải lên chỉ được kiểm, đóng dấu người xem trong tiến trình con
python -Icó trần bộ nhớ, CPU, thời gian (như OCR), tối đaMOC_RESTRICTED_MAX_PAGEStrang (mặc định 500). Trước đây pypdf chạy ngay trong svc-files, không trần trang, hai lượt cùng lúc mà chờ không hạn: một tệp nhiều trang tí hon làm treo mọi lần mở tệp hạn chế. Tải lên quá trần, quá nặng: 415restricted_pdf_onlykèmreason; mở tệp không đóng dấu được: 422unstampable; chờ lượt quá 10 giây: 503busykèmRetry-After. Test svc-files +14. -
Sổ kiểm toán không còn lộ danh sách được xem văn bản hạn chế (F3.7) cho lãnh đạo, quản trị đơn vị không đọc được văn bản:
overviewđã che nhưng cột thôentity_id(<văn bản>/<người được cấp>) vàactor_id(người cấp) thì không. Nay cả dòng bị lọc ở quyền Hasura (office.audit_log_visible); không sửa dòng sổ nào, chuỗi băm vàmake auditgiữ nguyên. Test CSDL +1. -
make envtạo.envquyền 600 (trước đây theo umask, thường 644: người dùng khác trên máy đọc được mật khẩu);make up,make install,make checktừ chối khi.envcòn giá trịchange_mevà in tên biến. -
Workflow Release kiểm tên thẻ đúng dạng
vMAJOR.MINOR.PATCHvà đọc phiên bản qua biến môi trường, không chèn${{ }}vào lệnh shell — tên thẻ chứa$( )không chạy được lệnh trong job có quyền ghi kho.