Tests only plus a cfg(test) gap seam between unlinkat and openat in attachment.rs and the
module doc sentence on the file_received receipt (follow-up receipt-outlives-failed-save,
accepted).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Config.fetch (exactly "1", read in from_args_and_env through the lookup), SaveAttachment
{room, event} on PersonaReply with the tempo_vote_beat_s present() pattern, a conditional
unrequired schema property, SAVE_FAULT refusal when the flag is off or the room/event is
out of bounds, Step::Answered carries zero to two notice lines (open, then save), and a
gated fetch step: NoStore decided before any request, the saver called with the receipt's
room id, one fixed line per outcome with the relative path only on success.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
attachment.rs (crate-private): save(store, sandbox, room_id, image) re-verifies through
chat_image::load_bytes (load keeps its signature and calls it), names the file
<sha256>.<ext> from the verified type, refuses a .git at the sandbox root or in
attachments/, and writes through rustix openat/linkat on held descriptors: temp name
.part unlinked first, 0600 in the create call, fsync, linkat (never replaces), bounded
nonblocking regular-file compare on EEXIST. cfg(windows) half returns WriteFailed.
rustix gains the fs feature; no crate added.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
First half of cut-persona-save-attachment.r7: the 4-slot request, the 5-slot receipt
(nested ChatImage) with its one validity table (Stored with nil receipt allowed), the
14 outcome words, ReceiptMode, and the exchange() fetch beside open_direct including
UnknownKind -> MouthRefused. Fixtures copied byte for byte from Bifrost 3a6b45d.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>