FDE PulseViệc làm FDE đang mở 639Mới trong 7 ngày 49Công ty đang tuyển 98Nhận làm từ xa 24%Lương trung vị (Mỹ) $203kTuyển nhiều nhất Databricks 122
EN

Tờ báo của nghề Forward Deployed Engineer

Bách khoa

Audit log cho agent: ghi cả người dùng lẫn agent làm thay họ

Khi agent làm sai trên hệ thống của khách hàng, một log chỉ ghi tên người dùng sẽ không trả lời được câu hỏi đầu tiên ai cũng đặt ra: ai đã làm việc này.

Ảnh nhóm hỗ trợ khách hàng làm việc trước màn hình máy tính, hoặc cận cảnh laptop hiển thị mã nguồn trong văn phòng.
Ảnh: Emile Perron / Unsplash
Chia sẻLinkedInFacebookX

Tóm tắt nhanh

  • OWASP yêu cầu mỗi bản ghi trả lời được khi nào, ở đâu, ai và cái gì. Với agent, phần "ai" phải gồm hai bên: người dùng và agent làm thay họ.
  • Delegation theo RFC 8693 giữ cho agent có danh tính riêng, còn impersonation thì xoá ranh giới đó. Token passthrough trong MCP cũng làm log ghi sai danh tính.
  • Tool input và output của model là văn bản không đáng tin. Cần làm sạch trước khi ghi, chỉ ghi nội dung khi được bật rõ ràng, và thêm cơ chế phát hiện chỉnh sửa.
So sánh hai bản ghi audit log. Trước: chỉ có trường sub là người dùng, không có act, nên hành động của agent trông như người dùng tự làm. Sau: có on_behalf_of là người dùng, actor là agent (được tô nổi bật) và actor_chain là các agent trước đó, nên agent giữ danh tính riêng.
Nếu log chỉ ghi sub, mọi hành động của agent trông như người dùng tự làm. Khi tách thêm actor và actor_chain, log trả lời được câu hỏi ai đã làm việc này. Nguồn: RFC 8693 (OAuth 2.0 Token Exchange), theo bài viết.

Thử hình dung một tình huống: agent hỗ trợ của khách hàng vừa đóng 40 ticket lẽ ra phải để mở. Log của hệ thống ticket ghi người thực hiện là trưởng nhóm hỗ trợ.

Thực tế anh ấy chỉ gõ một câu yêu cầu, còn agent tự chọn tool, tự điền tham số và chạy. Khi khách hàng hỏi ai chịu trách nhiệm, bạn không có bằng chứng nào để trả lời.

Lỗi nằm ở log, không phải ở model. Tài liệu bảo mật của Model Context Protocol mô tả đúng hiện tượng này: khi token bị chuyển thẳng qua (passthrough), log phía sau có thể ghi nhận request như thể đến từ một nguồn khác với một danh tính khác.

Bài hướng dẫn này dựng một audit log trả lời được ba câu hỏi: ai kích hoạt, agent nào hành động thay họ, và đã gọi tool nào với tham số gì.

Để làm theo, bạn chỉ cần Python 3 và một dự án Claude Code thử nghiệm để gắn hook. Các đoạn code dưới đây đã được giản lược để dễ học. Trước khi đưa vào môi trường thật, hãy đối chiếu cú pháp cấu hình hook với tài liệu Claude Code.

Schema đúng bắt đầu từ bốn câu hỏi

OWASP Logging Cheat Sheet đặt ra yêu cầu nền: mỗi bản ghi phải trả lời được khi nào, ở đâu, ai và cái gì, và phải có đủ thông tin cho việc giám sát, phân tích về sau. Với agent, câu hỏi “ai” khó hơn hẳn, vì luôn có ít nhất hai danh tính cùng tham gia vào một hành động.

RFC 8693 (OAuth 2.0 Token Exchange) phân biệt rõ hai cơ chế. Với impersonation, bên hành động nhận toàn bộ quyền của người gốc và log không còn tách được hai bên. Với delegation, agent vẫn giữ danh tính riêng. Vì vậy schema của bạn cần hai trường riêng, on_behalf_of cho người dùng và actor cho agent.

Câu hỏi Trường trong bản ghi Lấy từ đâu
Khi nào when (UTC, ISO 8601) Đồng hồ máy chủ
Ở đâu host, session_id Máy chạy, dữ liệu JSON của hook
Ai on_behalf_of, actor, actor_chain Claim sub và act trong token
Cái gì tool_name, tool_use_id, tham số Dữ liệu JSON của hook

Cách kiểm tra: đưa bảng này cho một đồng nghiệp và hỏi họ có trả lời được “agent nào đã đóng 40 ticket” chỉ từ các trường trên không. Nếu không được, schema vẫn còn thiếu.

Bước 1: bắt từng lần gọi tool bằng hook

Hook của Claude Code chạy ở mỗi lần gọi tool và nhận một gói JSON có session_id, tool_name, tool_input và tool_use_id. Bốn trường này đủ để biết tool nào được gọi, với tham số gì và trong phiên nào. Tài liệu hooks có sẵn một ví dụ PreToolUse ghi nối vào file log và dùng matcher để lọc các tool MCP, và đoạn code dưới đây dựa trên mẫu đó.

#!/usr/bin/env python3
# audit_hook.py - bản giản lược để học
import json, sys, os, hashlib, datetime

LOG = os.path.expanduser("~/agent-audit.jsonl")
event = json.load(sys.stdin)  # gói JSON hook nhận được

tool_input = event.get("tool_input") or {}
record = {
    "when": datetime.datetime.now(datetime.timezone.utc).isoformat(),
    "where": {"host": os.uname().nodename,
              "session_id": event.get("session_id")},
    "what": {"tool_name": event.get("tool_name"),
             "tool_use_id": event.get("tool_use_id"),
             "input_keys": sorted(tool_input.keys())},
}

Hãy đăng ký script này làm hook PreToolUse. Nếu chỉ muốn theo dõi tool MCP, thêm matcher như ví dụ trong tài liệu. Cách kiểm tra: cho agent gọi một tool bất kỳ rồi mở file log. Bạn phải thấy tool_use_id khớp với lần gọi vừa chạy.

Bước 2: tách người dùng khỏi agent

Hook cho bạn biết cái gì đã xảy ra, nhưng câu trả lời cho “ai” nằm ở token. RFC 8693 định nghĩa claim act trong JWT để thể hiện rằng có delegation: sub vẫn là người dùng, còn act cho biết ai đang hành động. Khi một agent gọi một agent khác, các act lồng nhau thành một chuỗi.

def who_from_claims(claims: dict) -> dict:
    chain, act = [], claims.get("act")
    while act:                      # act ngoài cùng = actor hiện tại
        chain.append(act.get("sub"))
        act = act.get("act")        # act lồng bên trong = actor trước đó
    return {"on_behalf_of": claims.get("sub"),
            "actor": chain[0] if chain else None,
            "actor_chain": chain}

Có một chi tiết dễ bỏ qua. RFC 8693 nói các actor trước đó trong chuỗi act lồng nhau chỉ mang tính thông tin, tức token không ép buộc gì với chúng. Muốn sau này truy lại được chuỗi đó thì chính log phải lưu nó. Đó là lý do có trường actor_chain.

Hàm này phải chạy ở phía server nhận request, vì token nằm ở đó. MCP khuyến nghị server chỉ chấp nhận token được cấp cho chính nó, vì token passthrough làm việc điều tra sự cố và kiểm toán khó hơn.

Nếu bạn để token của người dùng đi xuyên qua nhiều tầng, actor sẽ luôn rỗng và bạn quay lại đúng tình huống 40 ticket. Cách kiểm tra: giải mã một token thật mà agent đang dùng để gọi API, rồi xem sub là ai trước khi kết luận.

Nếu sub là người dùng nhưng không có claim act, token không thể hiện delegation nào: agent đang impersonation hoặc chuyển thẳng token của người dùng, và log phía sau sẽ không tách được hai bên. Còn nếu sub chính là agent, nghĩa là agent đang dùng token của riêng nó, thì việc thiếu act là bình thường vì không có ai được làm thay.

Trường hợp sau ghi đúng actor, nhưng token không nói gì về người dùng đã kích hoạt. Khi đó on_behalf_of phải lấy từ phiên làm việc của người dùng và ghi riêng, nếu không bản ghi sẽ chỉ còn một nửa câu trả lời cho câu hỏi “ai”.

Bước 3: coi mọi tham số là văn bản không đáng tin

Tham số của tool do model sinh ra, và model có thể đã bị prompt injection điều khiển. OWASP yêu cầu làm sạch toàn bộ dữ liệu sự kiện trước khi ghi để chống log injection.

Thử hình dung tham số chứa ký tự xuống dòng kèm một dòng JSON giả: nếu bạn ghi thẳng chuỗi đó, kẻ tấn công vừa tự tạo được một bản ghi trong log của bạn.

Cách phòng thủ đơn giản là luôn ghi bằng json.dumps. Hàm này escape ký tự xuống dòng nên mỗi bản ghi chắc chắn nằm trên đúng một dòng. Vấn đề thứ hai là dữ liệu cá nhân. Quy ước GenAI của OpenTelemetry cảnh báo rằng nội dung message nhiều khả năng chứa PII, nên chỉ ghi nội dung khi được bật rõ ràng.

if os.environ.get("AUDIT_LOG_CONTENT") == "1":   # opt-in
    record["what"]["input"] = {k: str(v)[:500] for k, v in tool_input.items()}

Mặc định, log chỉ giữ tên các khoá tham số. Như vậy vẫn đủ để biết agent gọi tool delete với khoá ticket_id mà không lưu email khách hàng. Giới hạn 500 ký tự là con số minh hoạ, hãy chọn theo nhu cầu của bạn.

Bước 4: làm cho log khó bị sửa lén

Một audit log mà ai đó sửa được thì không còn là bằng chứng. OWASP khuyên tích hợp cơ chế phát hiện khi bản ghi bị sửa hoặc bị xoá, và sao chép log sang kho chỉ đọc càng sớm càng tốt. Một cách đơn giản là nối chuỗi hash: mỗi dòng giữ hash của dòng trước nó.

def last_hash():
    try:
        with open(LOG, "rb") as f:
            return json.loads(f.readlines()[-1])["hash"]
    except (FileNotFoundError, IndexError):
        return "0" * 64

record["prev"] = last_hash()
body = json.dumps(record, sort_keys=True, ensure_ascii=False)
record["hash"] = hashlib.sha256(body.encode()).hexdigest()
with open(LOG, "a", encoding="utf-8") as f:
    f.write(json.dumps(record, ensure_ascii=False) + "\n")

Bản giản lược này chưa khoá file khi nhiều hook ghi cùng lúc, và nó chỉ giúp phát hiện chỉnh sửa chứ không ngăn được. Phần ngăn chặn đến từ việc đẩy log sang kho chỉ đọc. Để kiểm tra, bạn cần một script đi lại từ đầu file và tính lại từng hash theo đúng cách hook đã tính.

#!/usr/bin/env python3
# verify_chain.py - tính lại chuỗi hash từ dòng đầu tiên
import json, hashlib, os, sys

LOG = os.path.expanduser("~/agent-audit.jsonl")
prev = "0" * 64
with open(LOG, encoding="utf-8") as f:
    for n, line in enumerate(f, 1):
        rec = json.loads(line)
        h = rec.pop("hash")
        body = json.dumps(rec, sort_keys=True, ensure_ascii=False)
        if rec.get("prev") != prev or hashlib.sha256(body.encode()).hexdigest() != h:
            sys.exit(f"Đứt chuỗi tại dòng {n}")
        prev = h
print("Chuỗi nguyên vẹn")

Cách kiểm tra: chạy python3 verify_chain.py khi log còn nguyên, bạn phải thấy “Chuỗi nguyên vẹn”. Sau đó sửa tay một ký tự ở dòng thứ năm và chạy lại: script phải báo đứt chuỗi đúng tại dòng 5. Nếu bạn xoá hẳn một dòng, trường prev của dòng kế tiếp sẽ không khớp và script cũng dừng ở đó.

Bước 5: ghi cả lúc quyền được nới rộng

Hành động nguy hiểm thường đi kèm một lần xin thêm quyền. MCP khuyến nghị ghi các sự kiện nâng scope, gồm scope được yêu cầu và phần scope thực sự được cấp, kèm correlation ID. Tài liệu đó cũng cảnh báo rằng scope gộp quá rộng làm audit trail nhiễu và che mất ý định của người dùng.

Hãy dùng chung một correlation ID cho sự kiện cấp quyền và các lần gọi tool diễn ra sau đó. Khi điều tra, bạn sẽ đọc được trọn câu chuyện: người dùng yêu cầu gì, agent xin quyền gì, được cấp đến đâu, rồi đã dùng quyền đó vào việc gì.

Những lỗi thường gặp

Lỗi phổ biến nhất là ghi sub mà không ghi act, khiến mọi hành động của agent trông như người dùng tự làm. Lỗi thứ hai là bật ghi toàn bộ nội dung prompt “cho dễ debug”, và log trở thành kho PII không ai kiểm soát.

Lỗi thứ ba là để log trên cùng máy với agent, nơi agent có quyền ghi file, nên chính agent cũng có thể sửa được log của mình.

Nếu bạn muốn ghép vào hệ thống observability có sẵn, quy ước GenAI của OpenTelemetry định nghĩa thao tác invoke_agent và một thuộc tính định danh agent. Dùng chung tên trường với quy ước này giúp đội vận hành của khách hàng đọc được log mà không cần bạn giải thích.

Kỹ năng này xuất hiện ở đâu ngoài thực tế?

Thử hình dung đội bảo mật của khách hàng hỏi bạn sẽ truy lại hành động của agent thế nào khi có sự cố.

Nếu bạn mở ra được schema bốn câu hỏi, giải thích chuỗi act và chạy thử script phát hiện chỉnh sửa ngay trong buổi họp, cuộc trao đổi sẽ đi vào chi tiết thay vì dừng ở lời hứa.

Một FDE chỉ nói “chúng tôi có log” thì chưa trả lời được gì.

Khi đọc mô tả công việc FDE, hãy để ý các cụm như “enterprise security review”, “compliance” hay “agent governance”. Trong CV, đừng viết “đã làm logging”. Hãy viết rằng bạn đã thiết kế audit trail tách người dùng khỏi agent theo RFC 8693 và kiểm chứng được tính toàn vẹn bằng chuỗi hash, rồi mang theo file log để cho nhà tuyển dụng xem khi họ hỏi tiếp.

Agent càng được giao nhiều việc thì câu hỏi “ai đã làm việc này” càng hay được đặt ra. Câu trả lời thuyết phục nhất là một file log đã nằm sẵn trong kho chỉ đọc từ trước sự cố.

Bài này có hữu ích không?

Dùng cùng trợ lý AIHỏi Claude ↗Hỏi ChatGPT ↗
5 nguồn
Đọc tiếp trên lộ trình · Chặng 6: Đo lườngGit bisect cho agent LLM: tìm ra thay đổi làm agent của khách tệ đi chỉ sau vài vòng evalKhi khách báo "agent dạo này trả lời kém hẳn", phỏng đoán không giúp được bạn; tìm kiếm nhị phân trên lịch sử thay đổi cùng một eval chịu được nhiễu thì có.