path linkify: server attaches file_refs at message ingest

drop the /api/state-file/check probe endpoint (which let any
dashboard visitor enumerate filesystem layout by feeding paths)
and the client's optimistic-then-downgrade dance. instead, the
broker forwarder calls scan_validated_paths(body) — same
allow-list helper as the read endpoint — and attaches the
verified file tokens to DashboardEvent::Sent/Delivered as
file_refs: Vec<String>. /dashboard/history backfill does the
same per-row.

client appendLinkified takes a (text, refs) pair, walks
left-to-right linkifying every occurrence of any ref token,
longest-first tie-break. no regex, no probe, no cache, no
queue. when refs is empty/absent the body emits as plain text
(question/answer/reminder rendering — refs for those are a
follow-up).

operator inbox stores file_refs from the sent event so its
renderer gets the same anchors as the message-flow terminal.
This commit is contained in:
müde 2026-05-17 23:44:50 +02:00
parent 6e098fad29
commit 76e4034e01
5 changed files with 131 additions and 141 deletions

View file

@ -31,20 +31,31 @@ use crate::container_view::ContainerView;
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum DashboardEvent {
/// Broker `Sent` event mirrored onto the dashboard channel.
/// `file_refs` carries every path-shaped token in `body` that
/// hive-c0re verified is a regular file under the allow-listed
/// roots (per-agent `state/` + `shared/`). The forwarder
/// pre-validates so the dashboard doesn't need a probe
/// endpoint — the client renders anchors only for tokens that
/// appear in this list, everything else stays plain text.
Sent {
seq: u64,
from: String,
to: String,
body: String,
at: i64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
file_refs: Vec<String>,
},
/// Broker `Delivered` event mirrored onto the dashboard channel.
/// `file_refs` is the same shape as `Sent`.
Delivered {
seq: u64,
from: String,
to: String,
body: String,
at: i64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
file_refs: Vec<String>,
},
/// A new approval landed in the pending queue. Payload carries
/// enough to render the dashboard row without a `/api/state`