Chuyển đến nội dung
Filesystem backend và offload kết quả lớn

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_file trả về nội dung có đánh số dòng, cắt dòng ở 5000 ký tự, hỗ trợ offset/limit; grep giới hạn 1000 kết quả khớp; glob hỗ trợ ** và *.
  • edit_file thay một chuỗi chính xác; khớp không duy nhất mà không có replace_all là 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 version bê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ùng read_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}/files và 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 cho owner/admin và 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 text với kiểm tra kích thước theo byte; upload nhị phân nằm ngoài phạm vi.
  • grep chạ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ới hango.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_offloaded trên span execute_tool với gen_ai.tool.name, hango.fs.path, hango.tokens.before, hango.tokens.after.
  • Metric: counter hango.fs.operations (attribute op, error.type), counter hango.fs.bytes_written, gauge hango.fs.file_count theo từng tenant, counter hango.tool.result_offloads, histogram hango.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 — ToolWrapper thực hiện offload kết quả lớn.
  • internal/agent/tokens.go — interface Estimator vớ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) int vẫ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êm agent_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 qua fs.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

  1. Viết internal/fs/path.go trướ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.
  2. Viết migrations/00006_agent_files.sql và FileStore có scope với Get, Upsert (có kiểm tra version), ListPrefix, Delete, Grep (WHERE content ~ $pattern với LIMIT), 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.
  3. Viết internal/fs/namespace.go: factory đọc store.ScopeFromContext cộ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.
  4. Viết internal/fs/pgbackend.go cài đặt Backend trê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ề version của dòng. Edit đếm số lần xuất hiện và báo lỗi khi mơ hồ. Write/Edit truyền version mong đợi và đưa xung đột ra thành một kết quả lỗi. Write thực thi giới hạn byte.
  5. Viết internal/agent/tokens.go: Estimator với EstimateRequest(*provider.Request) int và Estimate(string) int, cộng một HeuristicEstimator (bytes/4 với các overhead cố định cho mỗi message và mỗi tool definition). Phase 10 thêm capability TokenCounter sau cùng interface đó.
  6. 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 sang tool.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.
  7. 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 version mới làm việc đó.
  8. Viết internal/agent/mwoffload.go như một ToolWrapper: gọi next, ước tính token trên Result.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à thay ForLLM bằng đầu/cuối cộng kích thước và hướng dẫn đọc. ForUser giữ 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 trong tool.Result, vốn không còn field Meta).
  9. Chèn cả hai vào DefaultStack ở slot Filesystem và cập nhật thứ tự được cố định trong mwstack_test.go.
  10. 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ộng PUT giới hạn cho owner/admin và 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ọi SetAuditAction với file.write hoặc file.delete, ghi agent id và đường dẫn vào detail và không bao giờ ghi nội dung file.
  11. 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, grep và 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ủa owner qua API thành công; test offload assert hình dạng bản xem trước và file đọc lại được qua read_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.
  12. 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ế: Backend là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ột ToolWrapper chứ 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: BackendProtocol với StateBackend/FilesystemBackend/StoreBackend/CompositeBackend, FilesystemMiddleware thực thi FilesystemPermission ở tầng tool, _message_eviction.py offload ở 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.operations theo op và error.type cho thấy một tool hoạt động sai; các span fs.* 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 event tool.result_offloaded giải thích việc kích thước prompt đột ngột giảm; GET /v1/agents/{id}/files cho 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_fetch lớ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_files nằ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_file vớ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 version mong đợ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.md bị 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ách owner thành công và với tư cách member trả về 403. Cả hai kết quả đều ghi một dòng audit_events với action file.write và outcome ok hoặc denied.
  • 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 span execute_tool, các event tool.result_offloaded thấy được, và các series hango.fs.operations/hango.tool.result_offloads tồn tại.

Đánh giá rủi ro

Rủi roKhả năng x Tác độngTín hiệuCách xử lý đã quyết trước
Path traversal hoặc thoát khỏi namespaceTrung bình x Nghiêm trọngMột lần đọc ngoài namespace thành côngNamespace 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 tenantCao x CaoMột grep từ session khác trả về history của session nàyCá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ữngTrung bình x CaoChỉ dẫn bị tiêm xuất hiện lại qua các runCá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 nhauTrung bình x Trung bìnhMất dữ liệu ghi mà không có lỗiKiể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 tenantTrung bình x CaoCác span fs.grep chậm, CPU database caoCá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 promptTrung bình x CaoOOM 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ầmCao x ThấpOffload kích hoạt quá sớm hoặc quá muộnHeuristic cố ý thận trọng và ngưỡng là config; phase 10 đổi estimator sau interface
Nội dung lọt vào traceTrung bình x CaoNội dung file thấy được trong TempoCá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ơnTrung bình x Trung bìnhCá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.