Filesystem backend và offload kết quả lớn
Tổng quan
Cho agent một filesystem ảo bền vững sau một interface Backend, cài đặt trên Postgres và được đặt namespace theo {tenant_id, agent_id}. Phơi nó dưới dạng một middleware cung cấp ls, read_file, write_file, edit_file, glob, grep với kiểm tra đường dẫn nghiêm ngặt và đọc có đánh số dòng. Sau đó dùng nó cho thắng lợi context có giá trị nhất hiện có: bất kỳ tool result nào vượt quá giới hạn token đều được ghi vào /large_tool_results/{tool_call_id} và được thay bằng một bản xem trước đầu/cuối cộng hướng dẫn đọc file. Backend này là điều kiện tiên quyết cho việc chia sẻ giữa các subagent, offload history, memory và skill.
Yêu cầu
Chức năng
- Interface
Backend:Ls,Read(offset/limit),Write,Edit(old/new, replaceAll),Glob,Grep,Delete,Stat. - Backend
pgstore: file là các dòng có khóa(tenant_id, agent_id, path)với nội dung, kích thước, mtime; namespace được suy ra từ context, không bao giờ từ tham số của tool. - Kiểm tra đường dẫn: chỉ đường dẫn tuyệt đối, đã chuẩn hóa, không có
..traversal, không có byte NUL, giới hạn độ sâu và độ dài đường dẫn, kích thước file tối đa (HANGO_FS_MAX_FILE_BYTES, mặc định 1MiB). read_filetrả về nội dung có đánh số dòng, cắt dòng ở 5000 ký tự, hỗ trợoffset/limit;grepgiới hạn 1000 kết quả khớp;globhỗ trợ**và*.edit_filethay một chuỗi chính xác; khớp không duy nhất mà không córeplace_alllà một tool result lỗi nêu số lần khớp.- Sửa cùng một đường dẫn hai lần trong một batch tool call bị từ chối bằng một tool result lỗi. Đây chỉ là một guard trong phạm vi batch: nó không nói gì về việc một agent cha và một agent con ghi đồng thời (phase 9), đó là lý do cột
versionbên dưới tồn tại. - `agent_files` có `version int not null default 1`. Mỗi `Write`/`Edit` thực hiện `UPDATE ... SET content=..., version=version+1 WHERE ... AND version=$expected`; kết quả không dòng nào được báo cho model dưới dạng một tool result lỗi ("file đã đổi kể từ lúc bạn đọc; hãy đọc lại và thử lại"), không bao giờ là một lần ghi đè im lặng. Đây là cơ chế duy nhất làm cho việc cha/con dùng chung filesystem an toàn.
- Offload kết quả lớn: bất kỳ tool result nào có số token ước tính vượt
HANGO_TOOL_RESULT_TOKEN_LIMIT(mặc định 20000) được ghi vào/large_tool_results/{session_id}/{tool_call_id}và model thấy 5 dòng đầu, 5 dòng cuối, tổng kích thước và một hướng dẫn dùngread_file. - **Namespace theo từng session cho các đường dẫn do máy ghi.** Kết quả bị offload nằm dưới `/large_tool_results/{session_id}/…` và phần offload history của phase 10 nằm dưới `/conversation_history/{session_id}.md`. `glob` và `grep` loại trừ hai prefix đó với mọi session khác session hiện tại, và files API áp dụng cùng việc loại trừ này, để transcript và payload tool của một session không đọc được bởi mọi lần chạy agent khác trong tenant. Việc đọc một đường dẫn tường minh nằm ngoài session hiện tại dưới các prefix đó trả về một tool result lỗi not-found.
- `/memories/` và `/skills/` là các prefix dành riêng, được thực thi trong `internal/fs/path.go`, chính hàm đã được audit mà từ chối traversal. Không có tầng định tuyến: chúng là các đường dẫn bình thường trong cùng backend với một quy tắc đặt tên và, từ phase 15, một quy tắc interrupt.
- Việc ghi vào `/memories/` và `/skills/` là đặc quyền, vì cả hai đều được đọc lại vào các prompt sau này nên là một bề mặt prompt-injection bền vững. Cho đến khi phase 15 tồn tại, chúng chỉ ghi được qua files API bởi `owner`/`admin`, và các tool của agent từ chối chúng bằng một tool result lỗi có giải thích. Từ phase 15 chúng mặc định có một quy tắc interrupt HITL mà config của agent có thể nới lỏng nhưng không thể bỏ đi một cách im lặng.
GET /v1/agents/{id}/filesvàGET /v1/agents/{id}/files/*phơi filesystem ở chế độ chỉ đọc qua API để kiểm tra;PUT /v1/agents/{id}/files/*ghi, giới hạn choowner/adminvà cho các prefix dành riêng.- Files API kiểm tra rằng agent thuộc tenant của người gọi **và** người gọi được phép thấy agent đó (giới hạn theo creator đối với `member`), trước khi bất kỳ đường dẫn nào được resolve. Agent id dùng để dựng namespace đến từ việc tra cứu agent trong phạm vi request, không bao giờ từ một đoạn đường dẫn do client kiểm soát.
Phi chức năng
- Mọi lời gọi backend đều bị giới hạn trong tenant ngay từ cách xây dựng: namespace factory đọc scope từ context và fail closed (lỗi thì từ chối).
- Test cô lập chéo tenant và chéo agent cho bảng
agent_files. - Nội dung được lưu dạng
textvới kiểm tra kích thước theo byte; upload nhị phân nằm ngoài phạm vi. grepchạy trong Postgres (position/~với một lần quét có giới hạn), không phải bằng cách nạp mọi file vào Go.
Kiến trúc
// internal/fs/backend.go
type Backend interface {
Ls(ctx context.Context, path string) ([]DirEntry, error)
Read(ctx context.Context, path string, offset, limit int) (File, error)
Write(ctx context.Context, path, content string) (WriteResult, error)
Edit(ctx context.Context, path, old, new string, replaceAll bool) (EditResult, error)
Glob(ctx context.Context, pattern, root string) ([]string, error)
Grep(ctx context.Context, pattern string, opt GrepOptions) ([]Match, error)
Delete(ctx context.Context, path string) error
Stat(ctx context.Context, path string) (DirEntry, error)
}
type GrepOptions struct {
Path string
Include string // glob filter
MaxCount int // default 1000
}
// internal/fs/namespace.go
type Namespace []string // {tenantID, agentID, "files"}
type NamespaceFactory func(ctx context.Context) (Namespace, error) // fail-closed
// Red team: F12 — internal/fs owns the agent-id context key; no other package defines or
// re-defines it, and the worker sets it through this constructor.
type agentKey struct{}
func WithAgentID(ctx context.Context, id uuid.UUID) context.Context
func AgentIDFromContext(ctx context.Context) (uuid.UUID, error) // fail-closed
// internal/fs/pgbackend.go
func NewPG(st store.FileStore, ns NamespaceFactory) Backend // Red team: F15 — was FileStoreFactory
// internal/fs/path.go
func Validate(p string) (string, error) // absolute, cleaned, depth/length capped, no traversal
// Red team: F14, F12 — reserved prefixes and session-scoped machine paths live here, so
// every caller inherits the same rules from one audited function.
const (
PrefixMemories = "/memories/"
PrefixSkills = "/skills/"
PrefixLargeResult = "/large_tool_results/"
PrefixHistory = "/conversation_history/"
)
func IsReserved(p string) bool
func SessionScoped(p string) (sessionID string, ok bool) // for the glob/grep/API exclusion
// internal/agent/mwfilesystem.go
type FilesystemMiddleware struct {
Backend fs.Backend
Limits fs.Limits
} // ToolProvider (6 tools) + PromptContributor (path conventions only) + ToolWrapper (offload)-- migrations/00006_agent_files.sql
create table agent_files (
id uuid primary key,
tenant_id uuid not null references tenants(id) on delete cascade,
agent_id uuid not null references agents(id) on delete cascade,
path text not null,
content text not null,
size_bytes int not null,
version int not null default 1, -- Red team: F12 — optimistic concurrency for shared access
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (tenant_id, agent_id, path)
);
create index agent_files_tenant_agent_path_idx on agent_files (tenant_id, agent_id, path);
create index agent_files_tenant_agent_updated_idx on agent_files (tenant_id, agent_id, updated_at desc);Instrumentation
- Span: các span INTERNAL
fs.{op}(fs.read,fs.write,fs.edit,fs.glob,fs.grep,fs.ls) vớihango.fs.path,hango.fs.bytes,hango.fs.matches,hango.agent.id; đường dẫn được ghi lại, nội dung thì không. - Offload wrapper ghi một span event
tool.result_offloadedtrên spanexecute_toolvớigen_ai.tool.name,hango.fs.path,hango.tokens.before,hango.tokens.after. - Metric: counter
hango.fs.operations(attributeop,error.type), counterhango.fs.bytes_written, gaugehango.fs.file_counttheo từng tenant, counterhango.tool.result_offloads, histogramhango.tool.result_tokens. - Trường log: mọi lần write và edit đều log ở mức info với
hango.agent.id, đường dẫn và độ chênh byte, để câu hỏi “ai đã đổi file này” trả lời được chỉ từ Loki.
Các file liên quan
Tạo mới
internal/fs/backend.go,internal/fs/namespace.go,internal/fs/path.go,internal/fs/path_test.go,internal/fs/pgbackend.go,internal/fs/pgbackend_test.go(integration),internal/fs/trace.go.internal/store/pg/file.go— phần SQL đằng sau backend (có scope).internal/agent/mwfilesystem.go,internal/agent/mwfilesystem_test.go.internal/agent/mwoffload.go,internal/agent/mwoffload_test.go—ToolWrapperthực hiện offload kết quả lớn.internal/agent/tokens.go— interfaceEstimatorvới một cài đặt heuristic. Phương thức chính của nó làEstimateRequest(req *provider.Request) int, đếm các system block, các message block, các tool definition và mọi fragment được chèn vào, không chỉ một string, vì mọi quyết định ngân sách về sau đều xét toàn bộ request.Estimate(string) intvẫn còn như một helper cho bước kiểm tra offload.internal/server/handlers_files.go.migrations/00006_agent_files.sql.docs/learning/08-virtual-filesystem-and-offload.md.
Sửa
internal/agent/mwstack.go— điền slot Filesystem (trước Subagent và Summarization) và đặt offload wrapper ngay bên trong nó để summarization về sau thấy các kết quả đã được offload.internal/store/store.go,internal/store/pg/scoped.go,internal/store/pg/tenancy_test.go(thêmagent_files).internal/config/config.go—HANGO_FS_MAX_FILE_BYTES,HANGO_FS_MAX_PATH_DEPTH,HANGO_TOOL_RESULT_TOKEN_LIMIT.internal/run/worker.go— đặt agent id vào context quafs.WithAgentID.internal/app/app.go— dựng backend và truyền nó cho stack factory.docs/system-architecture.md,docs/learning/README.md.
Các bước triển khai
- Viết
internal/fs/path.gotrước tiên, với một test bảng đầy đủ:/a/b,a/b(từ chối),/a/../../etc(từ chối),/a/./b(chuẩn hóa), NUL nhúng bên trong (từ chối), giới hạn độ sâu và độ dài, dấu gạch chéo cuối, cộng các helper cho prefix dành riêng và đường dẫn theo phạm vi session. - Viết
migrations/00006_agent_files.sqlvàFileStorecó scope vớiGet,Upsert(có kiểm tra version),ListPrefix,Delete,Grep(WHERE content ~ $patternvớiLIMIT),Glob(dịch glob sang một bộ lọc sơ bộLIKE/regex rồi khớp chính xác trong Go trên các đường dẫn trả về). Mọi query liệt kê đều loại trừ các đường dẫn/large_tool_results/và/conversation_history/của session khác. - Viết
internal/fs/namespace.go: factory đọcstore.ScopeFromContextcộng agent id từfs.AgentIDFromContext(key do package này sở hữu và worker đặt); nó trả về lỗi khi thiếu một trong hai. Validate mọi thành phần là một chuỗi UUID không rỗng. - Viết
internal/fs/pgbackend.gocài đặtBackendtrên file store.Readáp dụng offset/limit theo dòng, đánh số dòng%6d\t, cắt ở 5000 ký tự mỗi dòng và báo việc bị cắt, và trả vềversioncủa dòng.Editđếm số lần xuất hiện và báo lỗi khi mơ hồ.Write/Edittruyền version mong đợi và đưa xung đột ra thành một kết quả lỗi.Writethực thi giới hạn byte. - Viết
internal/agent/tokens.go:EstimatorvớiEstimateRequest(*provider.Request) intvàEstimate(string) int, cộng mộtHeuristicEstimator(bytes/4 với các overhead cố định cho mỗi message và mỗi tool definition). Phase 10 thêm capabilityTokenCountersau cùng interface đó. - Viết
internal/agent/mwfilesystem.go: sáu tool, mỗi tool mang văn bản chính sách trong chính mô tả của nó (không có văn xuôi trong system prompt), ủy quyền cho backend và ánh xạ lỗi sangtool.Errorf. Phần đóng góp prompt rất tối thiểu: một block nêu các đường dẫn gốc đang dùng và các prefix dành riêng. Việc ghi vào/memories/và/skills/bị từ chối bằng một tool result lỗi có giải thích cho đến khi phase 15 cài quy tắc interrupt. - Thêm guard sửa đổi trong phạm vi batch: middleware theo dõi các đường dẫn đã bị sửa trong batch tool call hiện tại trong private state có kiểu của nó và từ chối lần sửa thứ hai cùng đường dẫn trong batch đó; loop xóa tập này ở cuối batch. Ghi rõ trong tài liệu rằng guard này không bao phủ trường hợp cha/con đồng thời, cột
versionmới làm việc đó. - Viết
internal/agent/mwoffload.gonhư mộtToolWrapper: gọinext, ước tính token trênResult.ForLLM, và khi vượt giới hạn thì ghi toàn bộ text vào/large_tool_results/{session_id}/{tool_call_id}và thayForLLMbằng đầu/cuối cộng kích thước và hướng dẫn đọc.ForUsergiữ tham chiếu tới toàn bộ text. Ghi span event và metric (đường dẫn được ghi trên span, không phải trongtool.Result, vốn không còn fieldMeta). - Chèn cả hai vào
DefaultStackở slot Filesystem và cập nhật thứ tự được cố định trongmwstack_test.go. - Viết
internal/server/handlers_files.go: đọc (ls,read) cho bất kỳ người gọi nào được phép thấy agent, cộngPUTgiới hạn choowner/adminvà cho các prefix dành riêng. Cả hai resolve agent qua scoped store trước (tenant + khả năng nhìn thấy theo creator), rồi mới dựng namespace từ agent id đã resolve. Các đường dẫn do máy ghi của session khác bị loại khỏi danh sách. Các route có thay đổi dữ liệu gọiSetAuditActionvớifile.writehoặcfile.delete, ghi agent id và đường dẫn vàodetailvà không bao giờ ghi nội dung file. - Test: test bảng kiểm tra đường dẫn gồm cả prefix dành riêng và đường dẫn theo phạm vi session; integration test của backend cho từng thao tác gồm cả giới hạn grep và pattern glob; cô lập chéo tenant và chéo agent (agent A không đọc được file của agent B trong cùng tenant); cô lập session:
glob,grepvà danh sách files API của session B không thấy các mục/large_tool_results/hay/conversation_history/của session A, và việc đọc tường minh chúng thất bại; xung đột version: hai bên ghi với cùng version mong đợi cho ra một thành công và một tool result lỗi xung đột; việc tool của agent ghi vào/memories/bị từ chối trong khi một lần ghi củaownerqua API thành công; test offload assert hình dạng bản xem trước và file đọc lại được quaread_file; một test loop trong đó một tool trả về 100k ký tự và model request kế tiếp chứa bản xem trước, không phải payload. - Cập nhật docs và viết tài liệu học.
Dàn ý tài liệu học
docs/learning/08-virtual-filesystem-and-offload.md phải có:
- Khái niệm: vì sao một agent harness cần một lớp trừu tượng filesystem, và vì sao phình context là vấn đề trung tâm của kỹ thuật agent.
- Lựa chọn thiết kế:
Backendlàm điểm nối, store dựa trên Postgres có namespace{tenant, agent}, kiểm tra đường dẫn là một hàm đã audit riêng, đọc có đánh số dòng, grep trong SQL, offload là mộtToolWrapperchứ không phải code trong loop. - Đánh đổi: state backend (tạm thời) so với store backend (bền vững) so với đĩa thật; namespace theo tenant so với theo agent so với theo user và vì sao hango chọn
{tenant, agent}; heuristic ước tính token so với tiktoken. - Chiến lược context ba cơ chế và vì sao offload phải có trước summarization.
- So sánh với goclaw: các tool workspace với path jail trên đĩa thật; không có điểm nối
Backend, nên về sau không có chỗ để đặt memory hay skill. - So sánh với deepagents:
BackendProtocolvớiStateBackend/FilesystemBackend/StoreBackend/CompositeBackend,FilesystemMiddlewarethực thiFilesystemPermissionở tầng tool,_message_eviction.pyoffload ở 20000 token vào{artifacts_root}/large_tool_results/{tool_call_id}với bản xem trước 5 dòng đầu/cuối,MAX_LINE_LENGTH=5000,grep_max_count=1000, và việc từ chối sửa file song song. - Vì sao hango đi khác: store backend bền vững làm mặc định (state backend biến mất cùng thread), namespace được cố định bởi tenancy thay vì do người gọi cung cấp, quyền được hoãn cho allowlist của phase 7 cộng các interrupt của phase 15.
- Cách debug tầng này:
hango.fs.operationstheoopvàerror.typecho thấy một tool hoạt động sai; các spanfs.*dưới một span tool cho thấy chính xác đường dẫn nào được đọc hay ghi và lớn bao nhiêu; span eventtool.result_offloadedgiải thích việc kích thước prompt đột ngột giảm;GET /v1/agents/{id}/filescho phép kiểm tra agent thực sự đã ghi gì. - Namespace như một thuộc tính bảo mật: vì sao `{tenant, agent}` chưa đủ mịn cho các đường dẫn do máy ghi, và vì sao optimistic versioning tốt hơn một guard trong phạm vi batch cho truy cập dùng chung.
- Bài tập: ghi một file qua agent rồi đọc lại qua API; ép một lần offload bằng một
http_fetchlớn; thử một đường dẫn traversal và đọc lý do bị từ chối; cho hai bên ghi đua nhau và đọc kết quả xung đột. - Ngân sách thời gian: 13h cài đặt + 2h tài liệu học.
Tiêu chí thành công
-
go build ./... && go vet ./... && go test ./...đều qua. - Test bảng kiểm tra đường dẫn bao phủ traversal, đường dẫn tương đối, byte NUL, giới hạn độ sâu và độ dài.
- Integration test của backend qua cho cả tám thao tác trên Postgres thật.
- Agent A không đọc được file của agent B trong cùng tenant, và tenant B hoàn toàn không đọc được file của tenant A (hai test);
agent_filesnằm trong suite chéo tenant. - Một tool result 100k ký tự cho ra bản xem trước dưới 1000 ký tự trong model request kế tiếp và một file
/large_tool_results/{session_id}/{tool_call_id}đọc được (test). -
edit_filevới một khớp mơ hồ trả về tool result lỗi nêu số lần khớp, và file không đổi. - Hai lần ghi vào cùng đường dẫn trong một batch tool cho ra một tool result lỗi ở lần thứ hai.
- Một lần ghi đồng thời từ hai context với cùng
versionmong đợi cho ra đúng một thành công và một tool result lỗi xung đột; nội dung file khớp với bên thắng. - Session B không thể liệt kê, glob, grep hay đọc các đường dẫn
/large_tool_results/hoặc/conversation_history/của session A, qua các tool hay qua files API. - Một lần agent tool ghi vào
/memories/AGENTS.mdbị từ chối bằng một tool result lỗi có giải thích; cùng lần ghi đó qua files API với tư cáchownerthành công và với tư cáchmembertrả về 403. Cả hai kết quả đều ghi một dòngaudit_eventsvới actionfile.writevà outcomeokhoặcdenied. - Files API trả về 404 cho một agent mà người gọi không được thấy, trước khi bất kỳ xử lý đường dẫn nào chạy.
- Trong Grafana, các span
fs.*xuất hiện dưới các spanexecute_tool, các eventtool.result_offloadedthấy được, và các serieshango.fs.operations/hango.tool.result_offloadstồn tại.
Đánh giá rủi ro
| Rủi ro | Khả năng x Tác động | Tín hiệu | Cách xử lý đã quyết trước |
|---|---|---|---|
| Path traversal hoặc thoát khỏi namespace | Trung bình x Nghiêm trọng | Một lần đọc ngoài namespace thành công | Namespace chỉ đến từ context (key agent-id do internal/fs sở hữu) và là một phần của primary key; Validate là một hàm đã audit duy nhất với test bảng đầy đủ; các test chéo agent và chéo session là cổng kiểm |
| Transcript hoặc payload tool của một session đọc được bởi mọi lần chạy khác trong tenant | Cao x Cao | Một grep từ session khác trả về history của session này | Các đường dẫn do máy ghi có prefix theo session và bị loại khỏi danh sách glob/grep/API của session khác; test cô lập session là một tiêu chí thành công |
/memories/ trở thành một kho prompt-injection bền vững | Trung bình x Cao | Chỉ dẫn bị tiêm xuất hiện lại qua các run | Các prefix dành riêng cần đặc quyền để ghi: chỉ qua API và chỉ owner/admin cho đến phase 15, sau đó là một quy tắc interrupt HITL mặc định mà config của agent có thể nới lỏng nhưng không thể bỏ |
| Agent cha và agent con ghi đè file của nhau | Trung bình x Trung bình | Mất dữ liệu ghi mà không có lỗi | Kiểm tra version lạc quan được đưa ra cho model dưới dạng một kết quả xung đột có thể thử lại; guard trong phạm vi batch tường minh không bao phủ trường hợp này |
| Grep hoặc glob suy biến thành quét toàn bộ file của mọi tenant | Trung bình x Cao | Các span fs.grep chậm, CPU database cao | Các query luôn mở đầu bằng (tenant_id, agent_id) từ index; MaxCount và một statement timeout giới hạn công việc; span ghi số lượng khớp |
| File lớn làm tràn bộ nhớ hoặc prompt | Trung bình x Cao | OOM hoặc request quá cỡ | Giới hạn HANGO_FS_MAX_FILE_BYTES khi ghi, offset/limit khi đọc, offload trên tool result |
| Ước tính token sai đủ nhiều để offload kích hoạt nhầm | Cao x Thấp | Offload kích hoạt quá sớm hoặc quá muộn | Heuristic cố ý thận trọng và ngưỡng là config; phase 10 đổi estimator sau interface |
| Nội dung lọt vào trace | Trung bình x Cao | Nội dung file thấy được trong Tempo | Các span fs.* chỉ ghi đường dẫn và kích thước; collector loại bỏ các attribute nội dung như một lớp thứ hai |
StateBackend mặc định của deepagents trông đơn giản hơn | Trung bình x Trung bình | Cám dỗ lưu file trong run state | Đã chốt: store backend bền vững là mặc định, vì state backend biến mất cùng thread còn hango là multi-tenant và bền vững |
Rollback
goose down một bước sẽ xóa agent_files. Gỡ các slot Filesystem và offload khỏi DefaultStack đưa loop về hành vi của phase 7; chưa có package nào khác phụ thuộc vào internal/fs.