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.
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:
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ức | Cho phép gì |
|---|---|
read-only | Chỉ ghi được những chỗ bắt buộc như /dev/null |
workspace-write | Thêm thư mục làm việc và vùng tạm của backend |
danger-full-access | Bỏ 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ộ.
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
- Đừng dùng một biến boolean cho quyềnMột cờ
allowedkhô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. - Cho mỗi lần cho phép đúng một hành động
allowed-oncenghe phiền, nhưng nó là thứ giữ cho giao diện xin phép còn ý nghĩa. - Đặ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. - Đừ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
ApprovalPolicycó đúng hai giá trị,'ask'và'never';'ask'là mặc định và không có ai trả lời thì thànhunavailable, rồi từ chối.allowed-oncecho 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.mdvàapproval.mdkhông chứa một ngưỡng số nào.