Lần đầu tôi nối agent vào một API quản trị thật, tôi chỉ định dùng để list dữ liệu. Tool có endpoint GET /contacts, rồi tiện tay thêm POST /contacts/{id}/tags vì nghĩ trước sau cũng cần. Prompt hôm đó chỉ là “kiểm tra nhóm contact nào thiếu tag onboarding”.
Agent đọc danh sách, suy luận đúng, rồi hỏi có nên tag luôn không. May là host đang ở chế độ cần confirm. Nếu tool chạy auto, nó đã mutate dữ liệu production chỉ vì tôi expose action quá sớm.
Từ đó tôi có một rule khá khô: tool safety bắt đầu từ lúc thiết kế tool, không bắt đầu từ lúc prompt. Prompt có thể nhắc “đừng sửa”, nhưng nếu tool có quyền sửa và host cho phép gọi, bạn đang dựa vào model để tự kiềm chế. Đó không phải là control tốt.
Tool càng mạnh, boundary càng phải cụ thể
Trong AI coding, tool thường rơi vào ba nhóm:
- read-only: đọc file, search code, list issue, query logs;
- local side effect: edit file, run test, format code;
- remote side effect: gọi API, deploy, update database, gửi message.
Nhóm đầu ít rủi ro nhất. Nhóm cuối là nơi cần policy rõ. Một command npm test fail chỉ mất thời gian. Một API DELETE /users/{id} chạy sai có thể thành incident.
Vì vậy tôi không expose “admin client” tổng quát cho agent. Tôi expose task-specific tool nhỏ. Ví dụ thay vì một tool:
{
"name": "call_api",
"description": "Call any internal API",
"inputSchema": {
"type": "object",
"properties": {
"method": { "type": "string" },
"url": { "type": "string" },
"body": { "type": "object" }
}
}
}
Tôi thích tool hẹp hơn:
{
"name": "list_contacts_missing_onboarding_tag",
"description": "Read-only. List contacts that match the onboarding-tag audit query.",
"inputSchema": {
"type": "object",
"properties": {
"limit": { "type": "integer", "minimum": 1, "maximum": 100 }
},
"required": ["limit"]
}
}
Tool tổng quát tiện cho developer, nhưng quá rộng cho agent. Tool hẹp khó tái sử dụng hơn, nhưng audit được. Đây là tradeoff tôi chấp nhận khi dữ liệu thật nằm sau tool.
Read-only trước, mutation sau
Flow an toàn nhất cho tool mới là tách hai pha:
- Agent đọc và lập plan.
- Người hoặc workflow khác xác nhận rồi mới mutate.
Ví dụ audit tag:
Step 1: list candidates and explain why each candidate needs the tag.
Step 2: stop. Do not update tags.
Nếu sau đó cần update, tôi tạo tool riêng:
{
"name": "apply_onboarding_tag",
"description": "Mutation. Apply onboarding tag to one contact after explicit approval.",
"inputSchema": {
"type": "object",
"properties": {
"contactId": { "type": "string" },
"approvalId": { "type": "string" }
},
"required": ["contactId", "approvalId"]
}
}
approvalId có thể chỉ là một token do parent workflow tạo. Mục tiêu không phải security tuyệt đối, mà là buộc mutation có dấu vết phê duyệt. Nếu không có field này, agent không thể tự đi từ read sang write chỉ bằng suy luận.
Dry-run không phải option trang trí
Với tool có side effect, tôi muốn dryRun là default hoặc là mode bắt buộc chạy trước.
Ví dụ deploy tool:
{
"environment": "staging",
"version": "2026.05.25-1420",
"dryRun": true
}
Dry-run phải trả về thứ hữu ích:
{
"willUpdate": [
"service:web",
"service:worker"
],
"willRestart": [
"web-1",
"web-2"
],
"blocked": [],
"warnings": [
"No rollback image found for worker"
]
}
Dry-run kém là dry-run chỉ trả "ok": true. Nó không giúp người review quyết định.
Tôi cũng không thích flag force. Nếu thật sự cần force, đó thường là dấu hiệu tool đang thiếu một workflow riêng cho tình huống đặc biệt. force: true trong tay agent là invitation cho overreach.
Allowlist thay vì denylist
Denylist nghe an toàn nhưng thường thủng. Bạn block DELETE, agent vẫn có thể gọi POST /archive. Bạn block prod, agent gọi hostname alias.
Tôi dùng allowlist:
{
"allowedMethods": ["GET"],
"allowedHosts": ["api-staging.example.com"],
"allowedPaths": [
"/contacts",
"/contacts/search"
]
}
Nếu cần mutation, allowlist mutation cụ thể:
{
"allowedMethods": ["POST"],
"allowedHosts": ["api-staging.example.com"],
"allowedPaths": [
"/contacts/{id}/tags/onboarding"
]
}
Trong code, path matching phải strict. Đừng dùng startsWith("/contacts") rồi vô tình cho phép /contacts/delete-all. Dùng router hoặc pattern matcher rõ ràng.
Credential scope nhỏ hơn task scope
Một lỗi vận hành tôi thấy nhiều: dùng cùng API token cá nhân cho cả UI, script, và agent. Token đó có quyền rộng, không expiry rõ, và nằm trong env chung.
Với tool cho agent, tôi muốn credential riêng:
- chỉ access environment cần thiết;
- chỉ có scope read hoặc mutation cụ thể;
- rotate được mà không phá app chính;
- không commit vào repo;
- được load từ secret store hoặc
.local/gitignored.
Nếu token chỉ cần read staging, đừng cấp production write. Nếu tool chỉ cần list issue, đừng cấp repo admin.
Điểm này nghe hiển nhiên nhưng hay bị bỏ qua vì “để test nhanh”. Test nhanh với credential rộng là cách tạo thói quen xấu. Khi workflow chạy ổn, nó sẽ được dùng lại nhiều lần, và lúc đó quyền rộng trở thành rủi ro mặc định.
Audit log phải đọc được
Tool gọi API cần log đủ để điều tra, nhưng không log secret. Log tối thiểu:
- time;
- tool name;
- actor hoặc session id;
- environment;
- method/path hoặc operation name;
- dry-run hay apply;
- result summary;
- error code nếu fail.
Ví dụ:
{
"time": "2026-05-25T11:22:10+07:00",
"tool": "apply_onboarding_tag",
"actor": "agent-session-7b3c",
"environment": "staging",
"operation": "tag_contact",
"contactId": "cnt_123",
"dryRun": false,
"result": "success"
}
Không log bearer token. Không log full request body nếu body có PII. Nếu cần debug payload, redact trước.
Audit log không chỉ để security. Nó giúp bạn trả lời câu hỏi rất thực tế: “ai đã đổi cái này lúc 2 giờ chiều?”. Với agent, “ai” đôi khi là session, tool, hoặc PR. Phải lưu đủ để nối lại.
Human approval là điểm dừng, không phải câu văn
Nhiều prompt viết “ask for confirmation before destructive action”. Câu này tốt, nhưng chưa đủ. Điểm dừng phải nằm ở host hoặc tool layer.
Ví dụ tool delete phải fail nếu thiếu approval:
if (!input.approvalId) {
throw new Error("approvalId is required for destructive actions");
}
Nếu chỉ nhắc trong system prompt, bạn vẫn phụ thuộc vào model. Nếu enforcement nằm trong code, model có muốn làm nhanh cũng không vượt qua được.
Tôi phân loại action cần approval cứng:
- delete dữ liệu;
- deploy production;
- modify cloud resource;
- rotate credential;
- send external message;
- charge money hoặc tạo invoice;
- chạy migration irreversible.
Các action này không nên auto-run chỉ vì agent tự tin.
MCP không làm tool tự an toàn
MCP là protocol tốt để chuẩn hóa cách host nói chuyện với server. Nhưng protocol không tự quyết định policy cho bạn. Một MCP server expose tool run_shell với full permission vẫn nguy hiểm. Một server expose read-only resource rõ ràng thì an toàn hơn nhiều.
Tôi đánh giá MCP server theo câu hỏi:
- tool nào read-only;
- tool nào có side effect;
- input schema có hẹp không;
- auth scope là gì;
- host có approval gate không;
- log ở đâu;
- dry-run có thật không;
- failure mode có dừng lại không.
Nếu không trả lời được, tôi không nối vào workflow quan trọng.
Chốt lại bằng nguyên tắc
Tool tốt không phải tool làm được mọi thứ. Tool tốt làm đúng một việc, với quyền vừa đủ, có log, có dry-run nếu có side effect, và có điểm dừng khi rủi ro vượt ngưỡng.
AI coding đáng dùng vì nó giảm friction giữa intent và code. Nhưng khi nối intent đó vào API thật, friction không được về zero. Một chút ma sát ở đúng chỗ là thứ ngăn “audit thử” biến thành “đã sửa production”.