# Attachments and `spt fetch`

Sending a file with a message does not push bytes to the receiver. The file is
snapshot into this node's own store, registered for serving, and the message
carries its URL. The receiver decides whether to pull it, and when.

<!-- [doc->REQ-WEB-ATTACHMENT-PULL] -->
## Attaching a file to a message

```console
$ printf 'the report you asked for' | spt send doyle --attachment ./report.md
ATTACHED:report.md: http://localhost:5474/hfenduleam/f/report.md (2481 bytes, ttl 2592000000ms)
SENT:doyle
```

`--attachment` repeats; each file becomes its own registry entry with its own
URL. The message envelope grows an `attachments` attribute carrying, per file,
its served name, its URL and its size in bytes — so a receiving agent can decide
whether to fetch **before** it fetches. A core that has never heard of the
attribute delivers the message unchanged.

An attachment is a **snapshot**, and that is the one place the serving registry
does not resolve at request time:

| | `spt serve add` | `spt send --attachment` |
|---|---|---|
| What is served | the file at its path | a copy taken at send time |
| Editing the source | the reader sees the edit | the reader still sees what was sent |
| Deleting the source | the URL answers 404 | the URL keeps serving |
| Lifetime | until removed | a TTL, 30 days by default |

A message's attachment is as immutable as the message: an edit after the send
must not change what the receiver pulls, and deleting the original must not
turn a delivered link into a dead one.

Two sends of the same filename get two stable, distinct URLs under the
registry's ordinary disambiguation rule (`report.md`, then `report~1.md`), so no
link ever changes meaning.

A missing or unreadable path **refuses the send by name** — nothing is
delivered and nothing is spooled — rather than delivering a message whose link
is dead on arrival:

```console
$ printf 'here' | spt send doyle --attachment ./nope.md
ATTACHMENT_REFUSED:./nope.md: The system cannot find the file specified. (nothing was sent)
```

### Lifetime

`--ttl` sets this send's lifetime and **requires a unit** — `s`, `m`, `h` or
`d`. A bare number is refused rather than guessed, because `30` meaning seconds
when the sender meant days is a deleted attachment.

```console
$ printf 'valid for a day' | spt send doyle --attachment ./build.log --ttl 24h
```

The daemon's pulse reaps what has expired, deleting the snapshot file and its
registry entry together and logging how many it removed. An expired entry stops
serving the moment it expires, whether or not the reaper has run yet: between
expiry and the next tick the bytes are still on disk, and serving them would
make the TTL a suggestion.

<!-- [doc->REQ-WEB-FETCH-VERB] -->
## Pulling one: `spt fetch`

```console
$ spt fetch hfenduleam/f/report.md
report.md
$ spt fetch http://localhost:5474/hfenduleam/f/report.md ~/inbox/report.md
/home/reavus/inbox/report.md
```

It accepts a full node-prefixed URL or the bare `<node>/f/<name>` shorthand —
the shorthand is how a link is said between agents, and the host and port belong
to whichever node is doing the fetching. The written path is printed on stdout
so a caller can pipe it.

Without a destination the file lands in the current directory under its served
name. An existing destination is **not** overwritten without `--force`. The body
streams to a temporary file and is renamed into place, so an interrupted or
refused fetch never leaves a truncated file where a later reader would trust it.

The three outcomes are distinct, and the distinction is the point:

| Exit | Meaning |
|---|---|
| `0` | The file was written; its path is on stdout. |
| `3` | The owner **refused** it — an access decision. Retrying is wrong. |
| `1` | Anything else: unreachable owner, not found, a deadline, a local write error. |

Every fetch — local or remote — is served through the local daemon's web
surface, the same path a browser takes, so a fetch of your own node's file
exercises the code a remote fetch will use.

<!-- [doc->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER] -->
## The `FILE_ACCESS_HELPER` signal

An agent does not have to notice an attachment. When a delivered message carries
one, the now-signal hands the agent the exact command to run, taken verbatim
from the envelope:

```text
<FILE_ACCESS_HELPER>
spt fetch http://localhost:5474/kitsubito/f/report.md
</FILE_ACCESS_HELPER>
```

The same category answers the other way a file arrives: **a user quoting a
filepath**. When the user and the agent are on one node, nothing is registered
and the signal says the file is readable where it already is — serving a file to
a process that can already open it buys an audit entry and no access.

Guards, each of which produces silence rather than a bad line:

- The path must **exist** at signal time; a quoted path that is not there emits
  nothing rather than a dead link.
- **Absolute or `~`-rooted only.** A relative path has no anchor and would name
  a different file on another node.
- At most **five** per message.
- Once per (message, path): a re-poll in the same session emits nothing.

Every entry the helper registers is enumerable in `spt serve list` with its
origin, so an automatic exposure is exactly as visible as a deliberate one.

<!-- [doc->REQ-MSG-SHORT-ID] -->
## Reading a message back

Every message carries a short-ID, and an attachment link is part of what that id
renders — `spt msg show <id>` prints the message with its attachment URLs, and
`/<node>/m/<id>` is the same view in a browser. See
[Messaging](../messaging/overview.md) for the id itself.
