fix(telegram): resolve send_attachment paths in the user's workspace
Nightly Build / build (push) Successful in 8m6s
Nightly Build / build (push) Successful in 8m6s
`send_attachment` handed its `file_path` argument straight to `InputFile::file`, which resolves against the **server process's** working directory. Every path the model can actually have — relative to the user's home, or absolute inside their container — failed the `path.exists()` check, and the one class that didn't (a name that happens to exist next to the binary) would have sent the wrong file. The routing already exists for the fs-tools, so expose it rather than repeat it: `UserFilesApi` (core-api) reads a path in the agent's own vocabulary and is obtained from `UserChannelHandle::files()`, so it is scoped to one user by construction. skald-core implements it over `resolve_view_target` — host mount read directly, container-only path through `docker exec` — holding the `SharedFs` cell rather than a snapshot, so a remount lands without a login. The size cap is checked before the read (a new `exec_fs::size` for the container branch): the point of a cap is to keep an oversized file out of RAM, so checking it afterwards would protect nothing. A photo above `sendPhoto`'s narrower 10 MB ceiling goes out as a document instead of as an API error.
This commit is contained in:
@@ -22,6 +22,7 @@ pub mod provider;
|
||||
pub mod remote;
|
||||
pub mod tool;
|
||||
pub mod user_channel;
|
||||
pub mod user_files;
|
||||
pub mod user_fs;
|
||||
pub mod user_plugin_config;
|
||||
pub mod secrets;
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::approval::ApprovalApi;
|
||||
use crate::chat_hub::ChatHubApi;
|
||||
use crate::events::GlobalEvent;
|
||||
use crate::inbox::InboxApi;
|
||||
use crate::user_files::UserFilesApi;
|
||||
|
||||
/// Resolves an unlocked user's channel handle.
|
||||
///
|
||||
@@ -84,6 +85,13 @@ pub trait UserChannelHandle: Send + Sync {
|
||||
/// `approval()`/clarification/elicitation separately.
|
||||
fn inbox(&self) -> Arc<dyn InboxApi>;
|
||||
|
||||
/// The user's workspace files — reading a path in the agent's own vocabulary
|
||||
/// (`~/…`, `shared/{X}/…`, `/tmp/…`), routed to the host mount or to the
|
||||
/// container exactly as the fs-tools route it. A channel adapter that sends a
|
||||
/// file back to the user goes through this rather than the host filesystem,
|
||||
/// whose cwd is the server's and not the user's.
|
||||
fn files(&self) -> Arc<dyn UserFilesApi>;
|
||||
|
||||
/// Subscribe to the user's server→client event stream.
|
||||
/// Events are scoped to this user; no cross-user leakage.
|
||||
fn subscribe(&self) -> broadcast::Receiver<GlobalEvent>;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
//! Reading a user's files from a channel plugin (blueprint §6).
|
||||
//!
|
||||
//! A channel adapter that hands a file back to the user — Telegram's
|
||||
//! `send_attachment` is the first — is given a path in the **agent's** vocabulary
|
||||
//! (`~/report.pdf`, `uploads/{session}/photo.jpg`, `shared/{X}/…`, or a
|
||||
//! container-absolute `/tmp/out.png`), because that is the only vocabulary the
|
||||
//! model has ever seen. None of those spellings is a host path: resolving them
|
||||
//! means the same two-backing routing the fs-tools do — a bind-mounted path read
|
||||
//! host-side, anything else read through the user's container.
|
||||
//!
|
||||
//! That routing lives in the core, so this is the seam that lets a plugin borrow
|
||||
//! it instead of touching the process working directory (which is what a plain
|
||||
//! `std::fs::read` of an agent path does — it either fails or, worse, reads a
|
||||
//! same-named file next to the binary).
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
/// A file read out of a user's workspace.
|
||||
pub struct UserFile {
|
||||
/// The canonical agent-vocabulary path — what the user and the model see.
|
||||
pub display: String,
|
||||
/// Basename of [`display`](Self::display), for surfaces that need a file name.
|
||||
pub name: String,
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Reads files from one user's workspace, with the agent's own path routing.
|
||||
///
|
||||
/// Obtained from [`UserChannelHandle::files`](crate::user_channel::UserChannelHandle::files),
|
||||
/// so it is already scoped to that user: containment is the core's
|
||||
/// (canonicalize + prefix-check on the mounts, the container otherwise) and a
|
||||
/// path outside the caller's view is refused, never silently resolved elsewhere.
|
||||
#[async_trait]
|
||||
pub trait UserFilesApi: Send + Sync {
|
||||
/// Reads `path`, refusing anything larger than `max_bytes` **before** loading
|
||||
/// it — the cap is the caller's own limit (Telegram's upload ceiling, say),
|
||||
/// and a size check that ran after the read would protect nothing.
|
||||
///
|
||||
/// Virtual memory notes (`user-memory/…`, `shared-memory/…`) are not files and
|
||||
/// are rejected with a clear error.
|
||||
async fn read(&self, path: &str, max_bytes: u64) -> anyhow::Result<UserFile>;
|
||||
}
|
||||
Reference in New Issue
Block a user