Agent-First Files v0.2.0

by Agent-First Kit Contributors

An excluded file could still be read through a link pointing at it, because exclusions only looked at the name that was asked for. They check the resolved target now. Plus one slow read that could stall every other request, a watcher that deadlocked against its own reconfiguration, and truncated text previews that refused to show anything.

affiles makes two promises about a directory: nothing outside the root, and nothing the exclusions hide. The first was already checked against what is on disk rather than what a path looks like. The second was not, and that is the main fix here.

An exclusion is about the file, not the name

Exclusions were matched against the path a caller asked for. A symlink inside the root named notes.txt and pointing at an excluded .env was not excluded — its name did not match — so reading it returned the excluded file’s contents.

An exclusion now checks both the requested spelling and the resolved target, so a link to an excluded file or directory — or .ENV on a case-insensitive filesystem — is refused like the file itself, with the same not-found answer. Listing follows the same rule: a link whose target cannot be resolved is not listed, rather than guessed at, and a dangling link pointing into an excluded directory stays hidden. Entries are rechecked just before they are returned, since one can disappear or become excluded between the scan and the answer.

Hard links are the one case this cannot cover, and the docs say so: they are independent names for the same file, with nothing to resolve. Exclude each name that must stay hidden.

Two smaller listing fixes travel with it. A pattern that matches everything no longer hides the root itself, which left the projection with nothing to show, and excluded entries that sort first no longer shift page boundaries or cursors.

One slow read

FileRoot is synchronous, and the HTTP handlers called it directly on the async runtime. A read stuck on a slow disk or a network mount held a worker, so /health, other reads and the watcher could all wait behind it.

Filesystem work now runs on the blocking pool behind one boundary, and a failure at that boundary returns a plain files_io_error without a panic payload. The UI keeps at most one action in flight, in order, while change notifications keep flowing.

A watcher that waited on itself

Changing the watched set held its lock across the backend’s watch and unwatch calls. Those calls wait for the thread the change callback runs on, and the callback needed the same lock — so a rename or rebuild arriving while the set was being replaced deadlocked both. The lock is no longer held across backend calls.

A directory deleted and recreated under the same name could also stop being watched: the backend had already dropped the old inode’s watch, the unwatch failed, and the stale registration blocked the new one. It is forgotten even when unwatch fails, so the rebuilt directory registers again with the same interest.

serve stops cleanly on SIGTERM and SIGHUP as well as Ctrl-C, and the handlers are registered before the ready event is printed.

Truncated text is still text

A text preview is a window, by default the first megabyte. When that window ended in the middle of a multi-byte character, the whole read was refused as not_text. The incomplete tail — at most three bytes — is now omitted, and byte_length is the length actually returned, which is also a valid offset to read the cut character from. An invalid sequence inside the window, an unaligned starting offset, or an incomplete sequence at the real end of the file is still not_text.

A truncated image preview says preview incomplete before decoding, and keeps saying it if the partial image decodes, so a successful load does not imply a complete one.

Smaller things

Malformed or unknown query parameters return one invalid_query error with HTTP 400 and a fixed message, instead of a framework rejection that echoed the offending field or value. The OpenAPI document declares the permission and I/O failures every file operation can return, and lists invalid_cursor, which the API already used.

cargo install agent-first-files now builds the same CLI the release packages ship. The api feature used to be release-only, so a plain install lacked a verb the packages had; it is in the default set, and library users take default-features = false.

--version reports the build it came from, as every other spore’s does.

Breaking changes

Getting it

$ brew install agentfirstkit/tap/affiles
$ cargo install agent-first-files