Adds the file.read_exact operation so Basal can fetch one exact file from a live head's project, returning its bytes, size, and SHA-256. The call is admitted only on flow-read/ ephemeral scopes bound by Basal under a daemon-confirmed live head parent, and refuses every other tool, grant operation, and management route.
Reads wait for the parent head's queued scope registration before taking a policy snapshot, are capped at 1 MiB, and refuse non-regular and SQLite files. On Unix, files are opened by walking from a root descriptor with no-follow components via openat; parent directories use search-only handles so execute-only parents readable via a plain open are not refused. Before any byte is read, the handle's location and identity are rechecked. Handles that match the live SQLite registry by device, inode, and birth time are leaked rather than closed, so their close cannot drop SQLite's locks.
One child-capped deadline bounds the whole dispatch: daemon admission, parent-policy readiness, and executor queue/run. Policy reads poll try_lock within that budget; a lock still held answers file_read_exact_io_error. The parent snapshot is resolved on disk with aft.db unlocked, catalog and consent commits re-read the policy under the lock, and the resolved target is rechecked against a fresh snapshot before bytes return. Exhausting the budget on readiness answers PolicyTimeout.
Refusals use stable error codes and omit paths and policy details.
Also releases the daemon control writer handle during connection teardown so the ledger writer join no longer times out.
Confidence Score: 5/5
The changes appear safe to merge; no new actionable issue was found.
Summary
Adds file.read_exact for Basal reads under a live head's permissions.
AFT returns one bounded file read as bytes, size, and SHA-256.
AFT admits exact reads only through a short-lived route under a live head.
Grant and consent checks resolve head policy outside the database lock.
AFT releases its daemon writer handle when the connection ends.
All six earlier threads are unnumbered and resolved. The current code addresses their reported causes. None contains an explanatory dismissal or acknowledgment.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Basal exact-read request] --> B[Check child scope and deadline]
B --> C[Confirm parent is live]
C --> D[Wait for parent policy]
D --> E[Queue read under parent]
E --> F[Open checked file and read bounded bytes]
F --> G[Recheck parent policy]
G --> H[Claim reply and return bytes]
C --> T[Deadline reached]
D --> T
E --> T
F --> T
T --> U[Cancel work that has not started]
U --> V[Claim reply and return refusal]