- Thêm agent/roles/0_fix_dispatcher.md: phân tier/lane cho từng defect trước khi các agent khác chạy, kèm agent/commands/fix.md và hợp đồng đầu ra agent/output/dispatch_plan.md. - Cập nhật system/guardrail, response_policy, security và các checklist ui/ux/pr_readiness cho khớp luồng mới. - Mở rộng knowledge: i18n_rules, screen_map, theme_tokens, secrets_and_config; cập nhật workflow intake_to_fix và handoff_contract. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
18 KiB
Secret & Config — Nơi credential được phép nằm
Knowledge module dành cho
security-defect-fixer.
Nguồn chính
infrastructure/secrets/secret_store.pyinfrastructure/secrets/keyring_adapter.pyinfrastructure/config/schema_migration.pyconfig.pySECURITY.md
Lưu ý: Module này chỉ dành cho vấn đề security/config.
Ba module UI theme_tokens, i18n_rules, screen_map không xử lý credential.
1. Credential được phép lưu ở đâu?
Ưu tiên từ an toàn nhất → kém an toàn hơn:
| Bậc | Nơi lưu | Dùng cho | API / cách truy cập |
|---|---|---|---|
| 1 | OS Keyring thông qua SecretStore |
API key, token, mật khẩu thật | secrets.set/get/has/delete |
| 2 | Environment variable | Giá trị do admin đặt khi triển khai | _apply_env_overrides |
| 3 | config.json |
Chỉ dành cho config không bí mật | ctx.config.<group> |
| 4 | Hằng số trong source code | ❌ Không được chứa credential | — |
Rule quan trọng
Credential không được hardcode trong source code.
Nếu credential nằm trong code:
- Gate A có thể phát hiện.
- Credential có thể đã đi vào Git history.
- Xóa ở commit hiện tại không có nghĩa là credential đã biến mất khỏi Git history.
2. SecretStore — interface để làm việc với secret
SecretStore là interface (Protocol), không phải một hàm tiện ích.
File:
# infrastructure/secrets/secret_store.py
@runtime_checkable
class SecretStore(Protocol):
def get(self, key: str) -> str | None:
...
def set(self, key: str, value: str) -> None:
...
def delete(self, key: str) -> None:
...
def has(self, key: str) -> bool:
...
def provider_key(name: str) -> str:
return f"provider:{name}"
Ý nghĩa của từng API
| API | Ý nghĩa |
|---|---|
get() |
Lấy secret; thiếu key thì trả None, không được làm app crash |
set() |
Lưu secret |
delete() |
Xóa secret; không có key thì không cần báo lỗi |
has() |
Kiểm tra secret có tồn tại hay không mà không đọc giá trị |
provider_key() |
Chuẩn hóa cách đặt key cho provider |
Vì sao dùng Protocol?
Bản thật sử dụng OS Keyring:
- có thể chậm;
- có thể phát sinh exception;
- môi trường CI có thể không có keyring backend.
Do đó test không được truy cập keyring thật của máy.
Thay vào đó, test sử dụng FakeSecretStore.
Rule khi thêm secret mới
Không tự tạo cách đặt key mới.
Ví dụ đã có:
provider_key(name)
thì hãy dùng nó.
Nếu loại secret mới chưa có quy ước:
def xxx_key(...):
...
Hãy tạo một helper *_key() cạnh các helper hiện có.
Không rải string literal của key khắp source code.
Settings: kiểm tra secret bằng has()
Nếu UI chỉ cần biết:
"API key đã được cấu hình chưa?"
thì dùng:
secrets.has(key)
Không dùng:
secrets.get(key)
Chỉ để hiển thị dấu ✓.
Lý do: không cần đọc secret thật ra khỏi kho chỉ để kiểm tra trạng thái.
Khi KeyringAdapter.available == False
Có thể xảy ra khi:
- Linux không có keyring backend;
- CI;
- môi trường triển khai không hỗ trợ OS Keyring.
App phải có fallback phù hợp và không được crash chỉ vì keyring không khả dụng.
Bản thật là KeyringAdapter.
Service:
SERVICE = "cowork-local"
Có property:
available
3. Schema migration — thay đổi cấu trúc config an toàn
File:
infrastructure/config/schema_migration.py
Các thông tin chính:
CURRENT_VERSION = 2
ASSUMED_VERSION = 1
STEPS = {
1: _v1_to_v2,
}
Ý nghĩa:
CURRENT_VERSION: version config hiện tại.ASSUMED_VERSION: nếu file không cóschema_versionthì coi là version 1.STEPS: mỗi entry nâng đúng một version.
Ví dụ:
v1 → v2 → v3
Không được thiết kế kiểu:
v1 → v3
4 luật migration bắt buộc
4.1 Backup trước khi migration
Trước khi nâng schema:
backup()
tạo file dạng:
config.json.v<timestamp>.bak
Mục đích:
- người dùng vẫn có bản backup;
- app cũ có thể còn đọc được config cũ;
- migration lỗi vẫn có đường quay lại.
4.2 Chỉ nâng version, không hạ version
Nếu file config mới hơn version mà app hiện tại hiểu:
file version > CURRENT_VERSION
thì:
- log warning;
- giữ nguyên config;
- không cố đoán cách downgrade.
Không được tự ý biến config mới thành config cũ.
4.3 Mỗi migration là một function riêng
Ví dụ:
STEPS = {
1: _v1_to_v2,
}
Mỗi function xử lý đúng:
v(n) → v(n+1)
Không viết logic kiểu:
"Nếu thấy key office thì chắc đây là config cũ"
Version phải được xác định bằng schema_version.
4.4 Migration không nâng được version thì phải dừng
Nếu migration không thành công:
- không lặp vô hạn;
- không tự đoán;
- không tiếp tục nâng version giả;
- phải giữ trạng thái an toàn và báo lỗi/warning phù hợp.
4. Tiền lệ quan trọng: _v1_to_v2
Đây là migration quan trọng cần đọc trước khi thiết kế migration credential mới.
Migration này từng xử lý việc:
api_key
từ config file → SecretStore.
Mẫu chính:
def _v1_to_v2(data, secrets):
if secrets is None or not getattr(secrets, "available", True):
log.info(
"bỏ qua v1→v2: máy này chưa có kho bí mật dùng được"
)
return data
...
secrets.set(provider_key(name), key)
conf["api_key"] = ""
out["schema_version"] = 2
Có 2 bài học quan trọng
4.1 Không có Keyring thì không chuyển
Nếu Keyring không dùng được:
KHÔNG MIGRATE
Giữ nguyên version cũ.
Ví dụ:
v1 + không có keyring
↓
giữ nguyên v1
↓
lần sau có keyring
↓
migrate v1 → v2
Lý do:
Mất credential của người dùng còn tệ hơn việc trì hoãn migration.
4.2 Bỏ qua placeholder
Ví dụ:
api_key == "ollama"
chỉ là placeholder.
Không nên đưa placeholder vào Keyring.
Nếu không, Keyring sẽ chứa những secret giả không có giá trị.
5. ⚠️ Bẫy .get(key, fallback) với config đã deep-merge
Đây là một trong những bẫy quan trọng nhất của config.
Trong:
config.py:265
có:
_deep_merge(base, override)
Sau đó:
infrastructure/config/json_config_repository.py:90
config được merge với:
DEFAULT_CONFIG
Vì vậy config đưa tới UI đã có sẵn các default key.
Ví dụ DEFAULT_CONFIG có:
"sandbox_pw": ""
thì:
sec.get(
"sandbox_pw",
"<literal đã bị gỡ>"
)
sẽ trả:
""
chứ không trả fallback.
Vì sao?
dict.get(key, fallback) chỉ dùng fallback khi key không tồn tại.
Nhưng ở đây key đã được thêm bởi DEFAULT_CONFIG.
Hậu quả
Code như:
sec.get("sandbox_pw", "<safe fallback>")
có thể trông giống như có default an toàn.
Nhưng thực tế:
DEFAULT_CONFIG
↓
sandbox_pw = ""
↓
deep_merge()
↓
sandbox_pw luôn tồn tại
↓
.get(..., fallback) không bao giờ dùng fallback
Vì vậy fallback đó thực tế là dead code.
⚠️ Nguy hiểm hơn: chuỗi rỗng
Nếu code sau đó dùng:
entered == stored
thì:
entered = ""
stored = ""
sẽ trở thành:
True
Tức là input rỗng có thể mở khóa.
Đây là security bug S1.
Rule
Khi đọc credential từ config:
Không dựa vào fallback của .get() để tạo security default.
Thay vào đó:
- lấy giá trị thật;
- kiểm tra
None/rỗng một cách rõ ràng; - chỉ cho phép tiếp tục nếu credential hợp lệ.
6. Environment variable override
File:
config.py::_apply_env_overrides
Các biến hiện tại:
| Environment variable | Config được ghi vào |
|---|---|
COWORK_SANDBOX_PASSWORD |
agent_security.sandbox_pw |
COWORK_MS365_UNLOCK_CODE |
ms365.unlock_code |
COWORK_TEAMS_WEBHOOK |
teams.webhook_url |
COWORK_ACTIVE_PROVIDER |
active_provider |
COWORK_CA_BUNDLE |
tls_ca_bundle |
Environment override chạy sau deep-merge.
Do đó thứ tự ưu tiên là:
DEFAULT_CONFIG
↓
config.json
↓
environment variable
Environment variable có giá trị ưu tiên cao nhất.
Khi thêm credential mới
Hãy xem xét:
Có cần hỗ trợ environment variable để admin có thể cấu hình khi deploy hay không?
Không phải secret nào cũng bắt buộc phải có env override.
7. Sinh credential/token — dùng lại implementation có sẵn
File:
core/accounts.py:89
Hiện có:
_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
CODE_LENGTH = 12
def generate_code(existing_codes=None) -> str:
...
Alphabet bỏ các ký tự dễ nhìn nhầm:
I L O 0 1
Mục đích là người dùng có thể đọc và nhập lại code dễ hơn.
Rule
Dùng:
secrets
Không dùng:
random
Nếu cần access code cho người dùng:
generate_code()
Không tự viết thêm một generator khác.
Nếu token là token nội bộ và không cần người đọc:
secrets.token_urlsafe(32)
8. Gate A và Git history
Chạy:
python scripts/audit_security.py
Gate này quét:
.py;- config files;
- các vị trí có khả năng chứa secret.
Hiện repo có một số phát hiện đã tồn tại từ trước trong:
tests/test_project_context_*.py
Không được nhầm chúng với lỗi do patch hiện tại tạo ra.
Nếu credential đã xuất hiện trong Git history
Nếu phát hiện secret thật trong Git history:
1. Dừng phân phối
Không tiếp tục phát hành artifact có nguy cơ chứa credential.
2. Báo Cowork Team
Đây là vấn đề cần xử lý ở cấp team.
3. Không tự rewrite history
Không tự:
git filter
git rebase
force-push
nếu chưa có kế hoạch phối hợp rõ ràng.
4. Rotate credential
Credential đã lộ phải được xem là có khả năng bị compromise và cần rotate khi phù hợp.
Rule quan trọng
Xóa secret khỏi source code hôm nay:
KHÔNG XÓA SECRET KHỎI GIT HISTORY
Vì vậy fix_plan phải ghi rõ nếu credential từng xuất hiện trong history.
9. Quyết định phải hỏi Cowork Team
Thay đổi liên quan credential không được tự quyết chỉ vì:
CI xanh
Theo:
docs/governance/review-policy.md
credential-related change cần được security review phù hợp.
4 câu hỏi agent phải đưa cho người quyết định
1. Đây là loại nào?
- khóa chống bấm nhầm;
- hay credential/security mechanism thật?
Điều này quyết định mức độ bảo vệ cần thiết.
2. Lưu gì trong Keyring?
- plaintext;
- hay hash để kể cả admin cũng không đọc được?
Agent chỉ đề xuất, không tự quyết.
3. Người dùng hiện tại xử lý thế nào?
- giữ credential cũ;
- migrate;
- hay bắt buộc reset?
Đây là quyết định về backward compatibility và UX.
4. Credential được tạo ra hiển thị thế nào?
Cần xác định:
- có hiển thị cho người dùng không;
- hiển thị ở đâu;
- hiển thị trong bao lâu;
- người dùng được xem lại bao nhiêu lần.
10. So sánh credential — hai lỗi cần nhớ
Nguồn tham chiếu:
SEC-20260907-01
Đây là defect thật đã từng xảy ra trong repo.
Có hai bẫy liên tiếp.
10.1 Chặn chuỗi rỗng trước khi so sánh
Credential default thường là:
""
Do cơ chế deep-merge ở §5, giá trị rỗng này có thể đi thẳng tới code kiểm tra.
Nếu viết:
entered == stored
thì:
entered = ""
stored = ""
→ True
Đây là bypass bằng input rỗng.
Mẫu đúng đã có trong repo
Trong:
infrastructure/config/json_config_repository.py
có:
if (code or "") and code == self.ms365.get("unlock_code", ""):
Phần quan trọng là:
(code or "")
kết hợp với:
and
Nó đảm bảo code rỗng bị chặn trước khi thực hiện phép so sánh.
Rule
Credential rỗng:
MUST FAIL
Không được coi:
"" == ""
là thành công.
11. ⚠️ secrets.compare_digest() và Unicode
Một lỗi khác rất dễ mắc phải:
Thấy
==không an toàn về timing → đổi ngay sangcompare_digest().
Hướng đi đúng, nhưng phải kiểm tra miền input.
Ví dụ:
secrets.compare_digest("mật khẩu", "mật khẩu")
có thể gây:
TypeError
với str chứa ký tự non-ASCII.
Điều này đặc biệt quan trọng với Cowork Local vì app:
- mặc định dùng tiếng Việt;
- phục vụ khách Nhật;
- credential có thể chứa Unicode.
Mật khẩu có dấu không phải edge case.
Cách đúng: chuyển sang bytes
Dùng:
return secrets.compare_digest(
entered.encode("utf-8"),
stored.encode("utf-8"),
)
Như vậy phép so sánh hoạt động trên UTF-8 bytes.
12. Bài học tổng quát: API an toàn hơn có thể có input hẹp hơn
Đây là rule quan trọng cần nhớ khi review security.
Một API mới có thể:
an toàn hơn
nhưng đồng thời:
nhận ít loại input hơn
Ví dụ:
==
↓
compare_digest()
compare_digest() tốt hơn về timing attack, nhưng có thêm ràng buộc về kiểu dữ liệu/input.
Trước khi thay một API bằng phiên bản "an toàn hơn", phải kiểm tra
1. API mới nhận kiểu dữ liệu nào?
Ví dụ:
str;bytes;- ASCII;
- Unicode;
None;- empty string.
2. Input thật của app có nằm trong miền đó không?
Phải kiểm tra:
- EN;
- VI;
- JA;
- Unicode;
- độ dài;
None;- empty;
- boundary values.
3. Input ngoài miền sẽ xảy ra chuyện gì?
API mới có thể:
return False
hoặc:
raise TypeError
Không được giả định behavior.
4. Có regression test cho input đó chưa?
Đặc biệt phải test các input trước đây API cũ chấp nhận nhưng API mới có thể không chấp nhận.
13. Checklist nhanh cho security-defect-fixer
Trước khi tạo fix_plan, kiểm tra:
- Credential có đang nằm trong source code không?
- Credential có xuất hiện trong Git history không?
- Secret có nên nằm trong
SecretStorekhông? - Có thể dùng
provider_key()hoặc helper*_key()hiện có không? - UI có dùng
has()thay vìget()để kiểm tra trạng thái không? - Có xử lý
KeyringAdapter.available == Falsekhông? - Migration có backup trước không?
- Migration có chỉ nâng version không?
- Mỗi migration có một step rõ ràng không?
- Migration có dừng khi không thể nâng version không?
- Có đang dùng
.get(key, fallback)sai trên config đã deep-merge không? - Credential rỗng có bị chặn trước khi compare không?
- Nếu dùng
compare_digest(), input có thể là Unicode không? - Có chuyển credential sang UTF-8 bytes khi cần không?
- Có test
None, empty, Unicode, long và boundary input không? - Có cần environment variable override không?
- Có quyết định product/security nào cần Cowork Team không?
security_review: requiredđã được ghi trongfix_planchưa?
14. Nguyên tắc cuối cùng
Khi xử lý credential, luôn đi theo chuỗi:
Defect
↓
Xác định credential thật hay chỉ là UI guard
↓
Xác định nơi credential đang được lưu
↓
Trace 4 bước:
generate → store → read → compare
↓
Kiểm tra config deep-merge / DEFAULT_CONFIG
↓
Kiểm tra empty-input bypass
↓
Kiểm tra miền input của API bảo mật
↓
Kiểm tra migration + backward compatibility
↓
Kiểm tra Git history
↓
Xác định quyết định cần Cowork Team
↓
Tạo fix_plan
↓
security_review: required
Không tự thiết kế policy bảo mật thay cho Cowork Team.
Agent chịu trách nhiệm:
phát hiện
→ phân tích
→ chứng minh root cause
→ đề xuất phương án
→ ghi rõ rủi ro
→ route đúng
Agent không tự quyết những vấn đề thuộc policy, product hoặc security governance.