1. Tại sao cần nghĩ về quyền trong ChatGPT‑App (và rủi ro đặc thù ở đây)
Trong ứng dụng web “thông thường”, giữa người dùng và cơ sở dữ liệu của bạn chỉ có vài lớp: frontend, API, DB. Trong ChatGPT‑App, giữa người dùng và API xuất hiện thêm một tác nhân chủ động — LLM. Và nó không chỉ là “bộ lọc văn bản”, mà là một thực thể có thể:
- tự chọn công cụ nào cần gọi và với tham số nào;
- có thể bị tiêm prompt thông qua dữ liệu;
- có thể “nhầm” công cụ hoặc bịa ra tham số mà bạn không lường trước.
Nếu trao cho LLM quá nhiều đặc quyền, bạn sẽ gặp vấn đề kinh điển Confused Deputy: mô hình nghiêm túc làm những gì nó nghĩ rằng người dùng hay văn bản trong tài liệu yêu cầu, nhưng lại gọi delete_all_orders thay vì get_last_order.
Vì vậy mục tiêu của chúng ta là:
- Tối thiểu hóa quyền của auth_token (những dữ liệu và hành động nào được phép nói chung).
- Giới hạn những công cụ nào thực sự được mô hình dùng trong kịch bản cụ thể.
- Bổ sung kiểm soát của con người ở nơi hệ quả đặc biệt nghiêm trọng.
Và tất cả điều này cần làm mà không rơi vào hoang tưởng và cấm đoán mọi thứ, nếu không App sẽ trở nên vô dụng. Cân bằng giữa tiện lợi và an toàn — đó là nhiệm vụ chính của chúng ta trong mô-đun này.
2. Mô hình truy cập trong hệ sinh thái: ai chạm tới cái gì
Để không rối, hãy nhìn toàn bộ hệ thống. Chúng ta có vài tầng, mỗi tầng có phạm vi trách nhiệm và quyền riêng.
flowchart TD U[Người dùng trong ChatGPT] --> C[ChatGPT UI + LLM] C --> A["App của bạn (kế hoạch trực quan + widget)"] A --> G[MCP Gateway / API Edge] G --> S[Máy chủ MCP và microservices] S --> D[Cơ sở dữ liệu, hàng đợi, API bên ngoài]
Tóm tắt vai trò:
- ChatGPT UI và LLM: do OpenAI quản lý. Bạn đưa cho chúng chỉ dẫn (system‑prompt, mô tả tool), nhưng bạn không kiểm soát token nội bộ và quyền của nền tảng.
- App của bạn (kế hoạch, tools, widget): bạn quyết định công cụ nào khả dụng, mô tả ra sao, cần xác nhận UX nào, widget được phép hiển thị dữ liệu gì.
- MCP Gateway / API Edge: tại đây diễn ra kiểm tra token, ánh xạ userId, tenantId, danh sách scopes và định tuyến tới dịch vụ cần thiết.
- Máy chủ MCP và microservice: thực thi công cụ, truy vấn DB và API bên ngoài. Ở đây phải có kiểm tra nghiêm ngặt nhất: scopes, cô lập theo tenant, kiểm tra tính hợp lệ của đầu vào.
- Kho dữ liệu và API bên ngoài: tuyến phòng thủ cuối cùng (giới hạn ở mức DB, quyền của tài khoản dịch vụ bên ngoài).
Ý chính: LLM không phải nguồn cấp quyền truy cập. Mọi thứ đi vào máy chủ MCP được xem như “yêu cầu từ người dùng, do mô hình diễn đạt”. Việc quyết định có được thực thi thao tác hay không là nhiệm vụ của backend‑code của bạn, không phải của prompt.
3. AuthN vs AuthZ: những gì ta đã làm và những gì cần bổ sung
Trong mô-đun về xác thực trước đây, bạn đã:
- AuthN (Authentication) — xác định đây là ai. Qua OAuth 2.1/PKCE, ChatGPT nhận token từ IdP, sau đó đính kèm vào các lần gọi MCP. Trong đó có sub, user_id hoặc tương tự, đôi khi có tenant_id.
- AuthZ cơ bản — có thể bạn đã phân chia vai trò user/admin và kiểm tra tối thiểu “đây có phải là user” hay “admin” không.
Giờ làm bức tranh phức tạp hơn:
- mỗi auth_token phải mang tập scopes — các quyền dạng chuỗi resource:action, ví dụ catalog:read, orders:write, payments:create;
- máy chủ MCP của bạn phải kiểm tra sự phù hợp của các scopes với từng hành động, không chỉ “một lần ở đầu vào”;
- các công cụ khác nhau, thậm chí các thao tác khác nhau trong cùng một công cụ, có thể yêu cầu scopes khác nhau.
Theo thuật ngữ OAuth 2.1, ChatGPT là “public client”, MCP là “resource server”, còn OAuth‑server của bạn biết những scopes nào được hỗ trợ và ý nghĩa cụ thể của chúng. Metadata của tài nguyên MCP thường khai báo scopes_supported để ChatGPT có thể yêu cầu đúng các quyền cần thiết từ người dùng.
4. Thiết kế scopes cho GiftGenius
Lấy dự án học tập GiftGenius và xem các miền dữ liệu và hành động của nó. Về chức năng, ta có:
- xem danh mục và thẻ sản phẩm quà tặng;
- gợi ý dựa trên lịch sử;
- tạo đơn hàng;
- khởi động checkout / trừ tiền;
- chỉnh sửa danh mục ở chế độ admin.
Thay vì tạo một giftgenius:full_access “toàn năng”, tốt hơn là phân tách thành các scopes hợp lý.
Quy ước đặt tên: resource:action
Chiến lược resource:action hoạt động tốt, trong đó:
- resource mô tả miền: catalog, recommendations, orders, payments, admin.
- action mô tả loại hành động: read, write, đôi khi cụ thể hơn: create, delete, manage.
Ví dụ cho GiftGenius:
| Scope | Cho phép |
|---|---|
|
Đọc danh mục quà tặng công khai |
|
Đọc lịch sử gợi ý của người dùng |
|
Tạo đơn hàng mới |
|
Đọc lịch sử đơn hàng của người dùng |
|
Khởi tạo thanh toán / checkout |
|
Chỉnh sửa danh mục (chỉ cho admin‑UI/hỗ trợ) |
Người dùng GiftGenius thông thường sẽ cần (liệt kê bằng dấu cách): catalog:read recommendations:read orders:write orders:read payments:create. Đối với admin, thêm catalog:admin.
Quan trọng: đừng tạo *:* hay admin:all “vạn năng”. Càng chi tiết, càng dễ thu hồi một quyền cụ thể mà không phá vỡ toàn bộ ứng dụng.
Các loại scopes: read vs write vs critical
Hữu ích khi phân loại scopes về mặt tư duy:
- an toàn (read): không thay đổi trạng thái, tối đa là để lộ dữ liệu;
- gây biến đổi (write): tạo/thay đổi thực thể, tăng bộ đếm, nhưng không đụng đến tiền và không xóa hàng loạt;
- nghiêm trọng (critical): thanh toán, xóa tài khoản, xóa dữ liệu hàng loạt.
Với quyền nghiêm trọng, có thể áp dụng kiểm soát tăng cường:
- chỉ cấp cho số người dùng tối thiểu;
- yêu cầu người dùng đồng ý riêng trong UI ChatGPT khi cấp token;
- phía MCP yêu cầu xác nhận bổ sung (ví dụ, PIN dùng một lần — đây là kịch bản nâng cao).
Scopes trong mã: RequestContext và requireScope
Ở tầng MCP, tiện nhất là có một kiểu ngữ cảnh thống nhất:
// mcp/context.ts
export interface RequestContext {
userId: string; // ai
tenantId: string; // trong phạm vi tổ chức nào
scopes: string[]; // những quyền nào được cấp cho token
}
// Helper đơn giản để kiểm tra quyền
export function requireScope(
ctx: RequestContext,
needed: string
) {
if (!ctx.scopes.includes(needed)) {
throw new Error(`Missing scope: ${needed}`);
}
}
Giả định rằng bạn tạo RequestContext tại MCP Gateway sau khi xác thực token: giải mã JWT, kiểm tra chữ ký/thời hạn, lấy sub, tenant, scope — rồi gắn ngữ cảnh này vào mọi lần gọi công cụ.
Sau đó trong tool‑handler:
// mcp/tools/createOrder.ts
import { requireScope, RequestContext } from "../context";
export async function createOrder(
input: CreateOrderInput,
ctx: RequestContext
) {
requireScope(ctx, "orders:write");
// tiếp theo là logic tạo đơn hàng
}
Giờ thì ngay cả khi mô hình bất ngờ gọi createOrder ở nơi mà theo UX bạn không mong đợi, nếu thiếu orders:write thì công cụ sẽ không chạy.
securitySchemes ở cấp công cụ
Đặc tả MCP cho phép mỗi công cụ chỉ ra các sơ đồ ủy quyền và scopes mà nó cần. Trong ví dụ chính thức, securitySchemes được gắn trực tiếp vào mô tả công cụ.
Ví dụ giả định:
// mcp/server.ts
server.registerTool(
"createOrder",
{
title: "Create order",
description: "Creates a new order for current user",
inputSchema: {/*...*/},
securitySchemes: [
{ type: "oauth2", scopes: ["orders:write"] }
]
},
async ({ input }, ctx: RequestContext) => {
requireScope(ctx, "orders:write");
// ...
}
);
Ở đây có hai lớp bảo vệ:
- khai báo: ChatGPT biết rằng công cụ này cần orders:write và nếu thiếu quyền sẽ khởi động luồng auth (hoặc báo cho người dùng);
- mệnh lệnh: mã của bạn lại kiểm tra lần nữa trước khi thực hiện hành động thật.
Nếu có token nhưng thiếu scopes, máy chủ phải trả lỗi kèm WWW-Authenticate: Bearer error="insufficient_scope", scope="orders:write" — và ChatGPT có thể yêu cầu người dùng mở rộng quyền (step‑up authorization).
Insight
Trong các ví dụ chính thức có dùng securitySchemes. Tuy nhiên, nó chưa được phê chuẩn trong đặc tả chính thức theo đúng dạng viết trong ví dụ ChatGPT Apps SDK. Vì thế cần đánh dấu nó như phần mở rộng của giao thức chính thức — bọc trong _meta. Phiên bản hoạt động của ví dụ trên:
// mcp/server.ts
server.registerTool(
"createOrder",
{
title: "Create order",
description: "Creates a new order for current user",
inputSchema: {/*...*/},
_meta: { // như thế này
securitySchemes: [
{ type: "oauth2", scopes: ["orders:write"] }
]
}
},
async ({ input }, ctx: RequestContext) => {
requireScope(ctx, "orders:write");
// ...
}
);
5. Per‑tool permissions và các công cụ “nguy hiểm”
Scopes trả lời câu hỏi “auth_token này nói chung có thể làm gì”. Nhưng trong token còn có danh sách công cụ mà mô hình có thể dùng. Phần này cũng phải được thiết kế cẩn thận.
Phân loại công cụ
Tạm chia công cụ thành:
- thông tin (informational / read‑only): đọc dữ liệu, tạo báo cáo, tính toán không có hiệu ứng phụ;
- tác động (consequential): thay đổi trạng thái, trừ tiền, xóa thứ gì đó.
Tài liệu về ChatGPT Apps khuyến nghị: với công cụ read‑only thì đánh dấu rõ là an toàn, còn với công cụ nguy hiểm — mô tả hệ quả và bật thêm xác nhận UX.
Có thể làm qua:
- các annotation cho công cụ (các trường kiểu readOnlyHint, destructiveHint);
- mô tả bằng văn bản: “Công cụ này xóa đơn hàng vĩnh viễn”;
- cờ riêng confirmation_required, mà kế hoạch App của bạn dùng để chèn bước xác nhận vào hội thoại.
Xác nhận UX cho hành động nghiêm trọng
Ví dụ, GiftGenius có công cụ chargeCustomer (khởi tạo trừ tiền). Dĩ nhiên bạn không muốn mô hình gọi nó mà không có sự đồng ý của người dùng.
Trên cấp độ kế hoạch App, có thể trông như sau:
// app/plan/tools.ts (giả mã)
export const tools = [
{
name: "giftgenius.list_catalog",
description: "Hiển thị danh mục quà tặng",
annotations: { readOnlyHint: true }
},
{
name: "giftgenius.create_order",
description: "Tạo đơn hàng không kèm thanh toán",
annotations: { consequential: true }
},
{
name: "giftgenius.charge_customer",
description: "Trừ tiền cho đơn hàng",
annotations: {
consequential: true,
destructiveHint: true,
confirmationRequired: {
title: "Trừ tiền từ thẻ?",
message: "Sẽ thực hiện thanh toán cho đơn hàng N."
}
}
}
];
Tên trường cụ thể tùy phiên bản SDK, nhưng ý tưởng trùng với khuyến nghị: công cụ read‑only đánh dấu an toàn, công cụ nguy hiểm — yêu cầu xác nhận rõ ràng và giải thích kỹ trong mô tả.
Tiếp theo widget của bạn có thể phản hồi: nếu mô hình đề xuất gọi charge_customer, bạn hiển thị hộp thoại modal với nội dung dễ hiểu và chỉ sau khi nhấn “Xác nhận” mới thực hiện tool‑call thật.
Ví dụ component trong widget (đơn giản hóa):
// widget/components/ConfirmCharge.tsx
export function ConfirmCharge(props: {
orderId: string;
onConfirm: () => void;
}) {
return (
<div>
<p>Trừ tiền cho đơn hàng {props.orderId}?</p>
<button onClick={props.onConfirm}>
Đồng ý, xác nhận thanh toán
</button>
</div>
);
}
Mô hình khởi xướng ý tưởng “đến lúc thanh toán”, nhưng nút cuối cùng là do con người bấm. Đây chính là human‑in‑the‑loop mà giới an toàn rất ưa chuộng.
Công cụ chỉ dành cho agents/back‑office
Một trường hợp phổ biến khác: bạn có công cụ chỉ agents (theo nghĩa Agents SDK) hoặc admin nội bộ dùng, chứ không phải ChatGPT App “thông thường” của người dùng cuối.
Ví dụ, rebuildSearchIndex hay syncCatalogFromERP. Tốt nhất:
- đừng đưa vào danh sách tools chung cho App thường;
- cấu hình trong một agent/orchestrator riêng;
- bảo vệ bằng scopes riêng và có thể cả đường Auth riêng.
Nếu đơn giản thêm chúng vào danh sách công cụ khả dụng của App, bạn tăng rủi ro mô hình bất ngờ quyết định: “Hãy tái tạo index ngay bây giờ, biết đâu giúp tìm quà tốt hơn”.
6. Phân đoạn mạng và ranh giới tin cậy
Quyền không chỉ là scopes trên token. Trục lớn thứ hai là phân đoạn mạng và dịch vụ.
Bức tranh lý tưởng:
- bạn chỉ có một cổng vào public cho backend — MCP Gateway/Edge API;
- mọi thứ lưu trữ PII và tiền ở trong mạng riêng/VPC và chỉ truy cập qua gateway này;
- outbound từ backend bị giới hạn bởi danh sách domain được phép (allowlist: cổng thanh toán, CRM, microservice nội bộ).
Sơ đồ:
flowchart LR ChatGPT -- HTTPS --> Edge[API Gateway / MCP Endpoint] Edge -- private network --> MCP[MCP server] MCP -- private --> DB[(DB với PII)] MCP -- private --> SVC[Internal microservices] MCP -- HTTPS (allow) --> Stripe[Payments API]
Một vài quy tắc quan trọng:
- DB và dịch vụ nội bộ không phơi trực tiếp ra internet. Truy cập trực tiếp chỉ từ private‑network và chỉ từ những service thực sự cần.
- Edge/Gateway thực hiện auth và rate‑limiting. Chính nó kiểm tra token và scopes, chặn tần suất quá cao và ghi audit‑log chính.
- Kiểm soát egress. Máy chủ MCP không nên có khả năng gọi tới bất kỳ URL nào trên internet (tấn công SSRF, rò rỉ dữ liệu). Nên giới hạn rõ ràng danh sách host bên ngoài.
Trên thực tế, nếu bạn deploy MCP trên Vercel, Render hay Kubernetes, một phần những điều này không cấu hình thủ công được, nhưng vẫn có thể phân tách:
- dự án/cluster riêng cho dev/staging/prod;
- biến môi trường và khóa riêng cho mỗi môi trường;
- dịch vụ “edge” riêng (HTTP wrapper cho MCP) và các dịch vụ riêng tư tách biệt.
Tổng kết, ta đã có hai trục bảo vệ: quyền trên token (scopes) và ranh giới mạng. Thêm một trục nữa — đa tenant, khi cùng một App phục vụ nhiều tổ chức.
7. Multi‑tenant / bối cảnh tổ chức
Từ trước tới giờ ta nghĩ về một người dùng. Nhưng nhiều ứng dụng ChatGPT là đa thuê (multi‑tenant): cùng một App phục vụ hàng chục công ty. GiftGenius dễ dàng trở thành dịch vụ B2B cho doanh nghiệp: mỗi bộ phận có danh mục, ngân sách, đơn hàng riêng.
Tenant là gì và lấy ở đâu
Tenant thường là:
- tổ chức/công ty (Acme Corp);
- không gian làm việc (workspace);
- đôi khi là dự án hay môi trường.
Thuộc tính chính: dữ liệu của tenant này không được lộ cho tenant khác.
Trong luồng auth, tenant thường nằm trong:
- claim của token (tenant, org_id);
- tham số riêng trong yêu cầu ủy quyền (nhưng kém tin cậy hơn claim được IdP ký).
Quan trọng: chỉ tin tenantId lấy từ token đã kiểm chứng, không phải từ tham số công cụ. Nếu mô hình sinh ra {"tenantId": "acme"}, còn trong token của người dùng là tenantId: "globex", phải xem đó như một nỗ lực tấn công.
Tenant trong ngữ cảnh yêu cầu
Thêm tenantId vào RequestContext của chúng ta (đã làm ở trên) và không cho phép ghi đè từ đầu vào.
Kiểm tra cơ bản:
// mcp/tenant.ts
import { RequestContext } from "./context";
export function enforceTenant<TInput>(
input: TInput & { tenantId?: string },
ctx: RequestContext
) {
if (input.tenantId && input.tenantId !== ctx.tenantId) {
throw new Error("Tenant mismatch");
}
return { ...input, tenantId: ctx.tenantId };
}
Tiếp theo trong công cụ:
// mcp/tools/listOrders.ts
export async function listOrders(
input: { limit?: number; tenantId?: string },
ctx: RequestContext
) {
const safe = enforceTenant(input, ctx);
return db.order.findMany({
where: { tenantId: safe.tenantId },
take: safe.limit ?? 20
});
}
Chúng ta bỏ qua tenant từ tham số và gán cứng nó từ ngữ cảnh. Như vậy ngay cả khi LLM hoặc kẻ xấu “nhét” tenant khác, cũng không có tác dụng.
Cô lập tenant ở cấp cơ sở dữ liệu
Về kiến trúc có nhiều phương án:
- DB riêng cho mỗi tenant;
- schema riêng;
- một DB với tenant_id trong mỗi bảng và lọc nghiêm ngặt.
Dù chọn cách nào, quy tắc vàng là: không truy vấn nào tới DB được thực thi nếu thiếu lọc theo tenant_id lấy từ ngữ cảnh. Điều này đặc biệt quan trọng trong RAG/tìm kiếm vector: nếu quên lọc theo tenant, mô hình có thể tìm trên tài liệu của tổ chức khác.
8. Gắn kết với ứng dụng Next.js/Apps SDK của chúng ta
Giờ hãy ghép tất cả lại và xem scopes, tenant và ranh giới mạng hiện thực hóa trong dự án Next.js trên Apps SDK như thế nào. Ta sẽ thêm cụ thể và xem mã Next.js và Apps SDK.
Scopes và tenant nằm ở đâu trong dự án
Bố cục điển hình cho dự án học tập:
- Trong ứng dụng Next.js (Apps SDK), bạn có cấu hình App/connector và các trang cho OAuth callback.
- Trong máy chủ MCP — mã nhận HTTP/SSE từ ChatGPT, kiểm tra token và gọi công cụ phù hợp.
Đưa vào dự án những điều đã bàn:
- Trong cấu hình OAuth của tài nguyên MCP, khai báo scopes_supported cho GiftGenius (catalog:read, orders:write, v.v.).
- Trong cấu hình Apps SDK, mô tả App với danh sách công cụ và annotation của chúng (read‑only, consequential, luồng xác nhận).
- Trong máy chủ MCP, triển khai:
- phân tích và kiểm tra token;
- tạo RequestContext { userId, tenantId, scopes };
- các helper requireScope, enforceTenant, v.v.;
- mọi truy vấn DB luôn thông qua tenantId từ ngữ cảnh.
Ví dụ luồng “cách ly” để tạo đơn hàng
Thử theo dõi một kịch bản end‑to‑end.
- Người dùng viết: “Tạo đơn hàng cho bộ này với ngân sách $50”.
- Mô hình quyết định cần gọi giftgenius.create_order với tham số { productId, budget, ... }.
- ChatGPT kiểm tra: App có công cụ create_order không, công cụ đó cần scopes và securitySchemes nào. Hiểu rằng cần orders:write.
- Nếu token đã có và chứa orders:write, yêu cầu đi tiếp; nếu chưa — ChatGPT khởi động ủy quyền OAuth với scope cần thiết.
- MCP Gateway nhận yêu cầu, kiểm tra token, tạo RequestContext với userId=123, tenantId="acme", scopes=["catalog:read","orders:write",...].
- createOrder bên trong MCP:
- gọi requireScope(ctx, "orders:write");
- cố định tenant qua enforceTenant;
- tạo đơn hàng chỉ trong phạm vi tenantId="acme".
- Nếu đơn hàng yêu cầu thanh toán ngay, mô hình hoặc backend sẽ khởi tạo tiếp charge_customer, nơi:
- công cụ trong kế hoạch được đánh dấu confirmationRequired;
- widget render ConfirmCharge và yêu cầu người dùng xác nhận trừ tiền một cách rõ ràng.
Bằng cách đó, ta có phòng tuyến nhiều lớp: prompt quá rộng, prompt‑injection hay bug trong UX cũng không dẫn tới hành động mất kiểm soát, bởi ở đáy kim tự tháp vẫn có kiểm tra nghiêm ngặt về scopes, tenant và xác nhận thủ công cho hành động nghiêm trọng.
9. Những lỗi thường gặp khi thiết kế quyền và phân đoạn
Lỗi №1: Một scope “béo” kiểu app:full_access.
Cách này tiện khi demo nhưng nguy hiểm ở production. Mất một token — mất tất cả. Không thể thu hồi hay cấm một thao tác mà không ảnh hưởng tới phần còn lại. Hãy chia quyền theo miền và loại thao tác (read/write/critical).
Lỗi №2: Chỉ kiểm tra quyền “ở đầu vào” và không kiểm tra trong công cụ.
Đôi khi người ta làm vậy: “đã có token là làm được mọi thứ”. Và sau đó công cụ createOrder cứ thế được gọi, kể cả khi token đó không có orders:write. Cách đúng — kiểm tra scopes trong từng công cụ (hoặc ít nhất là middleware tập trung cho mọi thao tác gây biến đổi).
Lỗi №3: Không đánh dấu công cụ nguy hiểm và không yêu cầu xác nhận.
Nếu công cụ trừ tiền, xóa dữ liệu hoặc thay đổi quyền truy cập, nó không nên “trông” giống listCatalog. Thiếu annotation rõ ràng và xác nhận UX làm tăng khả năng mô hình gọi nó “chỉ vì thấy hợp lý”. Tối thiểu, hãy tách biệt công cụ read‑only và destructive, và đánh dấu rõ loại sau.
Lỗi №4: Tin tenantId từ tham số công cụ.
Một anti‑pattern rất thường gặp: công cụ getOrders({ tenantId }), trong đó tenantId đến từ mô hình. Nếu dùng trực tiếp, người dùng từ tenantA có thể truy cập dữ liệu của tenantB chỉ bằng cách chỉ định ID khác. Tenant phải đến từ token đã kiểm chứng và được gán cho mọi truy vấn DB và dịch vụ bên ngoài; giá trị người dùng đưa vào hoặc bị bỏ qua, hoặc được kiểm tra trùng khớp.
Lỗi №5: MCP/DB phơi trực tiếp ra internet.
Đôi khi trong prototype đơn giản, MCP server và DB phơi ra internet trên HTTP/5432 trần. Trong môi trường prod không được làm vậy: mọi truy cập phải đi qua một gateway/proxy bảo vệ duy nhất, và DB sống trong mạng riêng. Nếu không, bất kỳ endpoint dễ tổn thương hay webhook hở nào cũng là con đường thẳng tới dữ liệu.
Lỗi №6: Dùng cùng scopes/secret cho dev và prod.
Cách “yêu thích” để vô tình xóa dữ liệu prod trong lúc demo môi trường dev cục bộ. Mỗi môi trường phải có khóa, scopes và DB riêng. Ngay cả khi ai đó có token dev, họ cũng không thể gây hại dữ liệu prod.
Lỗi №7: Ngại “từ chối mô hình”.
Đôi khi dev lo lắng: “Nếu tôi thường trả lỗi insufficient_scope hay forbidden, mô hình sẽ hoạt động tệ hơn”. Thực tế, đó là hành vi bình thường và mong đợi: mô hình học được hành động nào khả dụng, hành động nào cần quyền bổ sung hay xác nhận. Tệ hơn là khi nó “thành công” làm điều không nên làm — ví dụ, thực hiện thanh toán lần hai.
GO TO FULL VERSION