Từ LLM đến HarnessPhần 10/11

Hỏi trước khi ghi, không hỏi được thì thôi

Quyền không phải một cờ đúng sai. Nó là một seam có hợp đồng, ba mức sandbox chỉ áp cho tệp, và một mặc định: không ai trả lời thì câu trả lời là không.

Một mũi tên đi vào cổng chắn dọc, phía sau là hai mũi tên ngắn bị chặn lại
TL;DR — Quyền không phải một cờ đúng sai. Nó là một seam có hợp đồng, ba mức sandbox, và một mặc định: không ai trả lời thì câu trả lời là không.

Cờ boolean không đủ

Cách dễ nhất để viết phần quyền là một biến allowed. Hỏi xong, đặt nó thành true, rồi chạy.

Cách đó hỏng ở ba chỗ, và cả ba chỉ lộ ra khi đã muộn.

Thứ nhất, true là vĩnh viễn. Người dùng duyệt một lệnh git status, và biến đó ở lại true cho lệnh rm tiếp theo.

Thứ hai, true không nói ai đã duyệt. Không có gì để kiểm toán.

Thứ ba, và tệ nhất: khi không ai trả lời được thì sao? Không có người dùng, không có kênh hỏi. Một biến boolean không có chỗ cho câu trả lời “không hỏi được”.

Harness tôi đọc biến nó thành hai kiểu có hợp đồng:

docs/subsystems/approval.md
type ApprovalPolicy = 'ask' | 'never'

Hai chế độ, không có chế độ thứ ba. 'ask' là mặc định. Còn 'never' thì mọi câu hỏi đều trả về 'rejected', không hỏi ai cả.

Và đây là chi tiết làm nên cả bài. Ở chế độ 'ask', khi chuỗi người trả lời không có ai, kết quả mặc định là unavailable — không phải “đồng ý”.

Không ai trả lời thì câu trả lời là không. Đó là mặc định, không phải một lựa chọn bạn phải nhớ bật.

Một lần cho phép, một hành động

Cơ chế cấp quyền có tên gọi rất cụ thể: allowed-once.

The action runs only after an approval service returns allowed-once and otherwise denies.— dsh, docs/subsystems/approval.md

Tên đã nói hết. Nó cho phép đúng hành động đã được hỏi, một lần. Không có allowed-for-session, không có allowed-for-tool.

Nghĩ tới vòng lặp của một agent: nó gọi write_file ba mươi lần. Với allowed-once, người dùng thấy ba mươi lời hỏi. Nghe phiền. Nhưng đó là cái giá để mỗi lần ghi đều có một người đã nhìn thấy nó.

Và có một điểm tinh tế trong hợp đồng: các nhánh rejected, cancelled và unavailable đều dẫn tới từ chối. Không có nhánh nào mặc định thành cho phép.

Bao cát chỉ nói về tệp

Đây là chỗ tôi muốn bạn đọc chậm nhất, vì nó là giới hạn bị hiểu sai nhiều nhất.

Bao cát có ba mức:

MứcCho phép gì
read-onlyChỉ ghi được những chỗ bắt buộc như /dev/null
workspace-writeThêm thư mục làm việc và vùng tạm của backend
danger-full-accessBỏ hẳn rào chắn

Và ngay cạnh định nghĩa đó, tài liệu đặt một câu giới hạn:

Network and process visibility are outside this vocabulary.— dsh, docs/subsystems/sandbox.md

Mạng và khả năng nhìn thấy tiến trình nằm ngoài từ vựng đó. Ba mức kia chỉ nói về thao tác trên hệ thống tệp.

Nghĩa là một lệnh chạy trong read-only vẫn có thể mở kết nối ra ngoài. Nếu bạn đọc “read-only” và tưởng nó là “cách ly”, bạn đã hiểu sai — và tài liệu không giấu điều đó.

Có một chi tiết nữa đáng chú ý: chỉ hai mức đầu gửi được cho backend. Mức thứ ba không gọi sandbox — nó chạy thẳng argv gốc.

Và mức độ thực thi được báo cáo như một dữ kiện, không phải một lời hứa. Mức full nghĩa là backend chi phối mọi hiệu ứng đã cam kết. Mức partial nghĩa là chỉ một phần.

Nhân Linux cũ và một số đường biên trên Windows là các ca partial hiện tại. Bên nào cần cam kết tuyệt đối thì phải từ chối, không được đoán.

Đừng gộp các kiểu từ chối

Khi một lệnh bị sandbox chặn, thông báo lỗi đến từ nhân hệ điều hành, không từ harness.

Harness không tự viết chuỗi từ chối nào. Nó chỉ ghi lại tiếng nói riêng của từng backend:

The selected backend’s denial DIALECT: the case-insensitive stderr substrings a file effect denied by THIS backend produces (EROFS text under bwrap’s read-only binds, EACCES under Landlock, EPERM under Seatbelt). A consumer that infers denials from a failed run’s stderr matches against exactly these rather than a cross-backend union — the union claims denials a given backend never produces.— dsh, docs/subsystems/sandbox.md

Ba backend, ba mã lỗi khác nhau: EROFS khi bwrap bind ở chế độ chỉ đọc, EACCES dưới Landlock, EPERM dưới Seatbelt.

Và lời cảnh báo ở cuối mới là phần đắt: đừng gộp chúng thành một union. Union sẽ khai báo những từ chối mà một backend cụ thể không bao giờ sinh ra — nghĩa là bạn bắt lỗi sai.

Đây là một nguyên tắc nhỏ mà đúng ở nhiều chỗ khác. Khi ba hệ thống nói ba thứ tiếng khác nhau, đừng dịch chúng về một câu chung rồi đoán.

Hai tài liệu, không một ngưỡng số nào

Tôi đọc sandbox.md và approval.md để tìm các con số quen thuộc và không tìm thấy gì.

Không timeout. Không số lần thử lại. Không luật kiểu “N lần là khoá”. Hai tài liệu đó không chứa một ngưỡng số nào.

Với một hệ thống bảo mật, đây là lựa chọn đáng chú ý. Mọi thứ khác trong harness đều có mặc định bằng số. 0,8 cho compaction. 30.000 ms cho tool web. 8.192 ký tự cho bộ cắt. Riêng phần quyền thì không.

Tôi đọc điều đó như một lời thừa nhận. Đúng số cho việc hỏi bao nhiêu lần là chuyện của bạn, không phải chuyện harness quyết hộ.

Chọn mức quyền theo khả năng hoàn tácTrục ngang là phạm vi quyền cấp, trục dọc là khả năng hoàn tác hành độngĐọc tệpRead-only,hoàn tác tự nhiênGhi workspaceHoàn tác đượcbằng gitGọi mạng ra ngoàiKhông thể thuhồi gói tinXoá ngoài workspaceNguy hiểm,bắt buộc duyệt từng lần
Hình 1 — Chọn mức quyền theo khả năng hoàn tác · tusgino
Xem dữ liệu diagram
type: quadrant
title: Chọn mức quyền theo khả năng hoàn tác
desc: Trục ngang là phạm vi quyền cấp, trục dọc là khả năng hoàn tác hành động
nodes:
  - id: q1
    label: Đọc tệp
    sub: Read-only, hoàn tác tự nhiên
  - id: q2
    label: Ghi workspace
    sub: Hoàn tác được bằng git
    focal: true
  - id: q3
    label: Gọi mạng ra ngoài
    sub: Không thể thu hồi gói tin
  - id: q4
    label: Xoá ngoài workspace
    sub: Nguy hiểm, bắt buộc duyệt từng lần
    focal: true
edges: []

Cái giá

Ba mươi lời hỏi cho ba mươi lần ghi là phiền. Người dùng sẽ bấm “cho phép” mà không đọc, và lúc đó giao diện xin phép trở thành nghi thức rỗng.

allowed-once không giải quyết vấn đề đó. Nó chỉ đảm bảo mỗi lần cho phép gắn với một hành động cụ thể. Muốn đỡ phiền, bạn phải làm việc ở chỗ khác. Thu hẹp quyền của agent lại, hoặc để nó chạy trong workspace nơi mức workspace-write đã đủ.

Và mặc định 'ask' có một cái giá riêng. Trong môi trường không có người — CI, cron, agent chạy nền — mọi câu hỏi đều thành unavailable. Mọi thao tác ghi đều bị từ chối.

Đúng về an toàn, và phiền về vận hành. Chỗ đó bạn phải chuyển sang 'never' một cách có ý thức, chứ không phải để nó tự diễn ra.

Đổi lại: mỗi lần ghi đều có một người đã nhìn thấy nó, và bạn kiểm toán được ai đã cho phép cái gì.

Làm được ngay

  1. Đừng dùng một biến boolean cho quyềnMột cờ allowed không nói được ai duyệt, duyệt cái gì, và mất hiệu lực khi nào. Hãy dùng một kiểu có hợp đồng.
  2. Cho mỗi lần cho phép đúng một hành độngallowed-once nghe phiền, nhưng nó là thứ giữ cho giao diện xin phép còn ý nghĩa.
  3. Đặt mặc định không trả lời là từ chốiKhông có người, không có kênh hỏi — kết quả phải là unavailable, rồi từ chối. Không bao giờ mặc định thành cho phép.
  4. Đừng gộp các chuỗi từ chốiMỗi backend có một phương ngữ riêng. Gộp chúng lại là bắt lỗi những ca chưa từng xảy ra.

Nguyên tắc

Quyền là một seam có hợp đồng, không phải một cờ. Và khi không ai trả lời được, câu trả lời là không.

Mặc định đúng của một agent có quyền ghi là “hỏi”, và khi không ai trả lời được thì câu trả lời là “không”. Mọi mặc định khác đều là đoán hộ người dùng.— tusgino
    • ApprovalPolicy có đúng hai giá trị, 'ask' và 'never'; 'ask' là mặc định và không có ai trả lời thì thành unavailable, rồi từ chối.
    • allowed-once cho phép đúng hành động đã được hỏi — không có phiên, không có cả tool.
    • Ba mức sandbox chỉ chi phối hệ thống tệp; mạng và khả năng nhìn thấy tiến trình nằm ngoài.
    • Mỗi backend có một phương ngữ từ chối riêng — EROFS, EACCES, EPERM — và không được gộp thành union.
    • sandbox.md và approval.md không chứa một ngưỡng số nào.

Câu hỏi thường gặp

Mặc định nên là hỏi hay không bao giờ hỏi?
Là hỏi. Với một agent có quyền ghi, im lặng không phải là đồng ý — và khi không ai trả lời được thì câu trả lời là không.
Sandbox bảo vệ được những gì?
Chỉ thao tác trên hệ thống tệp. Mạng và khả năng nhìn thấy tiến trình nằm ngoài từ vựng đó, và tài liệu nói thẳng như vậy.
Vì sao không gộp các chuỗi từ chối của mọi backend lại?
Vì mỗi backend từ chối bằng một kiểu khác nhau. Gộp lại sẽ khai báo những từ chối mà một backend cụ thể không hề sinh ra.
Một lần cho phép có hiệu lực tới đâu?
Tới đúng hành động đã được hỏi. Nó không mở quyền cho cả phiên hay cho mọi lời gọi cùng loại.
Tú (tusgino)Tú (tusgino)

AI Software Engineer tại Đà Nẵng. Viết về kiến trúc hệ thống production và cái giá phải trả của từng lựa chọn.