Tool Plane: bí mật, chính sách và an toàn
Sáu nguyên tắc, và với mỗi nguyên tắc là chỗ trong mã nó được ép buộc. Một nguyên tắc không có chỗ ép buộc là một câu khẩu hiệu.
| Nguyên tắc | Ép buộc ở đâu |
|---|---|
| 1. Đặc quyền tối thiểu | Mặc định từ chối trong policy-engine.ts; không grant thì không năng lực |
| 2. Cách ly chứng thư | security/secrets.ts là chỗ duy nhất đọc bí mật; agent không có đường tới |
| 3. Cách ly môi trường | environment_scope trên mọi grant và mọi chính sách |
| 4. Bảo vệ production tường minh | Chính sách prod-needs-human, ưu tiên 10 |
| 5. Kiểm toán đầy đủ | audit/logger.ts, gọi trên mọi nhánh, kể cả nhánh từ chối |
| 6. Truy cập theo năng lực | Tên tool trong registry, không phải endpoint API |
Bí mật
Danh sách những thứ không bao giờ rời khỏi Worker:
GITHUB_APP_PRIVATE_KEY
CLOUDFLARE_API_TOKEN
ADMIN_TOKENĐặt bằng wrangler secret put. Không trong wrangler.toml, không trong git.
Mỗi lượt đọc bí mật đi qua requireSecret() vì đúng một lý do: cần có một chỗ để kiểm khi hỏi "giá trị này có tới được agent không?". Câu trả lời là không, không gì trong module đó được trả về cho người gọi, đặt vào kết quả tool, hay ghi vào vết.
Thiếu bí mật thì trả về SECRET_NOT_CONFIGURED kèm tên bí mật và câu lệnh để đặt nó, tên thì có, giá trị thì không bao giờ.
Bộ che
Bí mật rò ra theo đường vòng chứ hiếm khi theo đường thẳng. Đường vòng hay gặp nhất là API bên ngoài vọng lại token trong thân lỗi. Nên redact() chạy trên: mọi thân phản hồi trước khi thành thông báo lỗi, mọi metadata trước khi vào bảng kiểm toán, và mọi kết quả operation trước khi lưu.
Nó bắt token GitHub (ghp_, ghs_, …), khối PRIVATE KEY, header Bearer, và JWT. Ngoài ra redactDeep() xoá trắng mọi khoá có tên nghi ngờ (token, secret, password, key, authorization) bất kể giá trị là gì, vì che theo hình dạng giá trị sẽ trượt những bí mật không có hình dạng nào cả.
redact sống trong module riêng (security/redact.ts) tách khỏi secrets.ts. Một vòng phụ thuộc giữa "đọc bí mật" và "giấu bí mật" là loại chuyện kết thúc bằng một token trong một dòng log.
Đối số không được sao chép vào vết
Bảng kiểm toán lưu hình dạng đối số, không lưu giá trị:
{ "owner": "dac2205", "files": "<array:3>", "content": "<string:5120>" }Nội dung file và thân PR không thuộc về một bảng kiểm toán, và thứ ai đó giấu trong đó cũng vậy.
Xác thực agent
Agent chứng minh danh tính bằng bearer token. Chỉ sha256 của token được lưu, nên rò cơ sở dữ liệu không phải là rò chứng thư agent. Token sinh ở POST /admin/agents và hiện ra đúng một lần; mất thì cấp lại, không lấy lại được.
Danh tính là thứ đầu tiên mọi request xác lập. Một quyết định phân quyền đưa ra mà không biết ai gọi thì không phải quyết định, mà là phỏng đoán.
Cấp lại và thu hồi
POST /admin/agents/:id/rotateĐây là đường duy nhất lấy lại quyền dùng khi một token mất, plane lưu băm, nên không gì trả lại được bản gốc. Nó cũng là đường thu hồi: một lượt ghi, và token cũ hết khớp ngay ở request kế tiếp.
Grant giữ nguyên, vì danh tính của agent không đổi, chỉ chìa khoá đổi. Tạo trùng id thì trả 409 kèm tên thao tác người gọi có lẽ đang muốn, chứ không phải một lỗi 500 từ ràng buộc UNIQUE.
Xoay không hồi sinh được một agent đã thu hồi: nó trả 409. Nếu không, xoay trở thành cửa sau đi vòng qua thu hồi, mà token cấp ra cũng chẳng đăng nhập được vào đâu, nên phát nó ra chỉ là nói dối người gọi.
Thu hồi một agent
DELETE /admin/agents/:idKhông xoá hàng khỏi agents, và đó là chủ ý. audit_logs.agent_id trỏ về bảng đó, nên xoá cứng sẽ biến mọi hành động đã ghi vết của agent ấy thành một id không ai tra được. Một hệ lấy ghi vết đầy đủ làm nguyên tắc thì không được sở hữu một thao tác bào mòn chính vết đó, nhất là một thao tác dọn dẹp.
Ba lượt ghi khiến agent hết dùng được ngay và vĩnh viễn:
status = 'revoked', lớp xác thực từ chối mọi trạng thái khácactive;token_hashbị thay bằng một giá trị không ai giữ, nên bản token đã phát ra, kể cả bản đã rò, không còn khớp với gì;- grant bị xoá sạch, để nếu sau này có ai bật lại thì nó bắt đầu từ không quyền nào.
Gọi lại lần nữa trả ok chứ không phải lỗi: thu hồi là trạng thái, không phải sự kiện.
Header x-conan-user cho biết agent đang hành động thay mặt ai. Nó làm sắc nét vết kiểm toán và không bao giờ nới rộng một quyền.
Chính sách toàn tổ chức
('prod-needs-human', 'Production actions require human approval', 'require_approval',
'*', '*', '*', 'production', 10, 1)Đọc là: với mọi agent, mọi tool, mọi tài nguyên, khi môi trường là production → cần người duyệt. Ưu tiên 10 nên nó xét trước mọi thứ khác.
Không grant nào gỡ được nó. Muốn gỡ thì phải sửa hàng này trong D1, một hành động có chủ đích, để lại vết, và ai cũng thấy.
Hàng thứ hai, no-self-deploy, chặn mọi agent deploy chính Tool Plane. Một hệ thống mà agent nâng cấp được lớp kiểm soát của chính nó thì không còn lớp kiểm soát nào.
Giới hạn tần suất
60 lượt/phút cho mỗi cặp agent × tool, đếm theo cửa sổ cố định trong D1.
Mục đích không phải kiểm soát chi phí, mà là bán kính thiệt hại: một agent kẹt trong vòng lặp phải đụng tường từ rất lâu trước khi nó mở hai trăm pull request.
Vết kiểm toán
Mọi hành động đặc quyền để lại một hàng, kể cả những hành động bị từ chối, và đó mới là những hàng đáng đọc. Một chuỗi PERMISSION_DENIED liên tiếp từ một agent là một trong hai chuyện: quyền cấp thiếu, hoặc agent đang thử làm việc không ai định cho nó làm. Cả hai đều cần người nhìn.
{
"timestamp": "2026-09-02T00:00:00Z",
"agent_id": "engineering-agent",
"user_id": "user-123",
"tool_id": "github.create_pull_request",
"resource": "dac2205/conanplatform",
"environment": "development",
"decision": "allow",
"decision_reason": "Agent grant allow for github.create_pull_request … [agent-grant]",
"status": "success",
"duration_ms": 820
}decision_reason ghi luật nào đã quyết định, kèm nguồn (agent-grant, policy:prod-needs-human, default-deny, registry). Không có nó, mỗi lần điều tra một lượt từ chối là một lần dựng lại toàn bộ trạng thái phân quyền trong đầu.
Ghi vết là "cố gắng hết sức" theo nghĩa một lượt ghi hỏng không được làm hỏng một lượt gọi hợp lệ, nhưng nó hiện ra ở log Worker, không bị nuốt.
Duyệt của người, đúng một lần
Một phê duyệt chỉ dùng được khi: đúng agent đó, đúng tool đó, đúng tài nguyên đó, đúng môi trường đó, chưa hết hạn (60 phút), và trạng thái đang là approved. Thiếu bất kỳ điều nào đều có nghĩa "không có phê duyệt", không bao giờ có nghĩa "gần đúng rồi".
Việc tiêu thụ là một UPDATE … WHERE status = 'approved' rồi kiểm số hàng đã đổi. Hai request đồng thời thì đúng một cái thắng.
Ranh giới bề mặt
/admin/* dùng ADMIN_TOKEN và so sánh theo thời gian hằng. Không agent nào có token đó, và không tool nào gọi tới /admin. Nếu một tool nào đó có thể, thì lớp quản trị đã không còn tồn tại.
Điều Tool Plane không bảo vệ
Nói thẳng ra, vì tin vào một lớp bảo vệ không có là tệ hơn không có lớp nào:
- Không xét ý định. Tool Plane không biết một PR là đúng hay sai; nó chỉ biết agent được phép mở PR. Chất lượng thay đổi là việc của review, không phải của lớp này.
- Không chống prompt injection. Nếu một agent bị nội dung ngoài dẫn dụ gọi một tool nó có quyền gọi, Tool Plane sẽ cho chạy. Cái nó bảo đảm là injection không mở rộng được quyền, và mọi thứ đã xảy ra đều có vết.
- Không phải sandbox. Code agent chạy nằm trong Sandbox, là một hệ khác.