Persona saves a named attachment into her sandbox (BIFROST_CHAT_FETCH, off by default) #64

Merged
eureka merged 4 commits from eureka/persona-save-attachment into main 2026-11-12 02:07:13 +00:00
Owner

What

The Persona's saver for the mouth's attachment fetch: on her turn the Persona may name one image in a room she can say in; Huginn asks the mouth (chat_fetch_request.v1), loads the stored bytes, re-verifies them (hash, type, bounds), and saves them into her sandbox under a hash-named file (0600 in a 0700 dir, no symlink follows, O_EXCL temp then link); a failed save is told to her in her own next turn. Specs: eureka-body:cut_spec:cut-persona-save-attachment.r7 (with cut-mouth-fetch-attachment.r7, Bifrost PR #73 merged as d4365a8). Reports cut-persona-save-attachment.h1, .h2.

State

  • Flag off by default (BIFROST_CHAT_FETCH, exactly 1 enables; off: no request, no schema property, a reply naming the field is refused OutOfBounds).
  • Not deployed. Shipper: Eureka. Why: attachment saver under specs r7 (Kiko, 2026-11-11, "a Mind keeps a Matrix file she can see in her sandbox"), Soul passes s1 and s2, Heimdall's 2026-11-12 receipt ruling.
  • Rebased onto main ecf147d (clean, no conflicts; main's persona.rs/self_run.rs changes merged automatically); tests rerun after the rebase: mind-body --lib 972 passed, 1 ignored; chat_rudp 22; dependency_graph 1; public_surface 2 (under unshare -r; the control/persona tests refuse a tmpdir not owned by root otherwise).
  • persona-chat-v9 fixtures are byte-identical to Bifrost's persona-chat-v7 at d4bccd4 (diff -r, sha256).

Receipt meaning (accepted, eureka-body:follow_up:receipt-outlives-failed-save)

The file_received receipt means only that the mouth fetched and stored the file for her; it does not prove the file reached her sandbox. A failed save leaves the receipt standing and she is told the save failed. Sentence is in attachment.rs's module doc.

Open

  • Lows left in force (Soul s1): refusal-told-as-unanswered (a typed refusal other than UnknownKind is told as "may or may not have gone through"; inherited from open_direct), part-name-linked-by-name (linkat takes the .part name, so her own shell could swap the file between fsync and linkat; her own sandbox only).
  • Windows write path unchecked (cfg(windows); no target here). cargo-mutants ran on persona.rs, chat_client.rs and chat_image.rs (zero survivors) and attachment.rs (the survivors are equivalent or in the Windows/crash-seam arms).
  • Huginn's flag is gated on the mouth update (Bifrost d4365a8, root-list-mouth-fetch-20261112.md steps A and B) and on Void's 'fetched for' wording in the client; both in eureka-body:follow_up:huginn-flag-gates.

🤖 Generated with Claude Code

## What The Persona's saver for the mouth's attachment fetch: on her turn the Persona may name one image in a room she can say in; Huginn asks the mouth (`chat_fetch_request.v1`), loads the stored bytes, re-verifies them (hash, type, bounds), and saves them into her sandbox under a hash-named file (0600 in a 0700 dir, no symlink follows, `O_EXCL` temp then link); a failed save is told to her in her own next turn. Specs: `eureka-body:cut_spec:cut-persona-save-attachment.r7` (with `cut-mouth-fetch-attachment.r7`, Bifrost PR #73 merged as `d4365a8`). Reports `cut-persona-save-attachment.h1`, `.h2`. ## State - **Flag off by default** (`BIFROST_CHAT_FETCH`, exactly `1` enables; off: no request, no schema property, a reply naming the field is refused `OutOfBounds`). - **Not deployed.** Shipper: Eureka. Why: attachment saver under specs r7 (Kiko, 2026-11-11, "a Mind keeps a Matrix file she can see in her sandbox"), Soul passes s1 and s2, Heimdall's 2026-11-12 receipt ruling. - Rebased onto main `ecf147d` (clean, no conflicts; main's `persona.rs`/`self_run.rs` changes merged automatically); tests rerun after the rebase: `mind-body --lib` 972 passed, 1 ignored; `chat_rudp` 22; `dependency_graph` 1; `public_surface` 2 (under `unshare -r`; the control/persona tests refuse a tmpdir not owned by root otherwise). - `persona-chat-v9` fixtures are byte-identical to Bifrost's `persona-chat-v7` at `d4bccd4` (`diff -r`, sha256). ## Receipt meaning (accepted, `eureka-body:follow_up:receipt-outlives-failed-save`) The `file_received` receipt means only that the mouth fetched and stored the file for her; it does not prove the file reached her sandbox. A failed save leaves the receipt standing and she is told the save failed. Sentence is in `attachment.rs`'s module doc. ## Open - Lows left in force (Soul s1): `refusal-told-as-unanswered` (a typed refusal other than `UnknownKind` is told as "may or may not have gone through"; inherited from `open_direct`), `part-name-linked-by-name` (`linkat` takes the `.part` name, so her own shell could swap the file between `fsync` and `linkat`; her own sandbox only). - **Windows write path unchecked** (`cfg(windows)`; no target here). `cargo-mutants` ran on `persona.rs`, `chat_client.rs` and `chat_image.rs` (zero survivors) and `attachment.rs` (the survivors are equivalent or in the Windows/crash-seam arms). - Huginn's flag is gated on the mouth update (Bifrost `d4365a8`, `root-list-mouth-fetch-20261112.md` steps A and B) and on Void's 'fetched for' wording in the client; both in `eureka-body:follow_up:huginn-flag-gates`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>
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>
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>
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>
eureka merged commit 232764bf78 into main 2026-11-12 02:07:13 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/Huginn!64
No description provided.