feat: outbound file attachments (agent → Discord)#635
Open
chaodu-agent wants to merge 3 commits intoopenabdev:mainfrom
Open
feat: outbound file attachments (agent → Discord)#635chaodu-agent wants to merge 3 commits intoopenabdev:mainfrom
chaodu-agent wants to merge 3 commits intoopenabdev:mainfrom
Conversation
Agents that include `` markdown in their replies will have the file uploaded as a native Discord attachment and the marker stripped. Security: only files under ~/.oab/outgoing/ are permitted. The agent must explicitly copy files there — no configurable allowlist needed. - OutboundConfig in config.rs (opt-in, disabled by default) - extract_outbound_attachments in media.rs (regex + canonicalize + size cap) - OutboundRateLimiter in outbound_rate.rs (per-channel sliding window) - ChatAdapter::send_file_attachments with default no-op - DiscordAdapter override via serenity CreateAttachment::path - 13 new tests (8 outbound extraction + 5 rate limiter) Closes openabdev#298. Addresses openabdev#355.
Add magic bytes validation (PNG, JPEG, GIF, WebP, BMP) to prevent data exfiltration via text files. An agent tricked by prompt injection into dumping env vars or secrets to a .txt file in the outgoing dir will now be blocked. - is_image_file() checks file header magic bytes - Text/binary files are rejected with a warning log - 2 new tests: blocks_text_file_exfiltration, accepts_real_png
Defines the four-layer responsibility model (infra → OAB → agent CLI → user), explains the images-only rationale, documents the signed-URL pattern for non-image files, and prepares enterprise FAQ answers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Agents running through OpenAB can receive files from users, but have no native path to send files back to the chat. This PR adds an opt-in, config-driven, rate-limited pathway: agents include
markdown in their reply, OpenAB validates the path, uploads the file as a native attachment, and strips the marker from the visible text.Closes #298. Addresses #355.
Credits
This PR is based on the design and research from:
Both PRs were auto-closed due to rebase/discussion-URL requirements. This PR reimplements the feature on current
mainwith two key design changes (see below).Design Change 1: Hardcoded
~/.oab/outgoing/DirectoryPR #300/#401 used a configurable
allowed_dirslist (default:/tmp/,/var/folders/). This PR replaces that with a single hardcoded directory:~/.oab/outgoing/.Why:
cpfiles to the outgoing dir, which is a deliberate action (not an accidental markdown reference)canonicalize+Path::starts_within agent prose won't accidentally trigger uploadsTrade-off: agents need one extra step (
cpto~/.oab/outgoing/), but this is trivial for any coding agent with filesystem access.Design Change 2: Image-Only Restriction (Anti-Exfiltration)
PR #300/#401 allowed any file type. This PR restricts outbound attachments to image files only, validated by magic bytes (PNG, JPEG, GIF, WebP, BMP).
The threat model:
Even with a locked-down outgoing directory, a prompt-injected agent could:
env > ~/.oab/outgoing/leak.txt(dump secrets)(exfiltrate via attachment)The outgoing directory prevents OAB from reading arbitrary paths, but it cannot prevent the agent from copying sensitive data into it. The agent has full filesystem access — that's the agent runtime's security boundary, not OAB's.
Our mitigation: validate file content via magic bytes. Only files whose first bytes match known image format signatures are accepted. This blocks the most obvious exfiltration vector (text/binary dumps) while preserving the primary use case (screenshots, diagrams, generated charts).
What this does NOT prevent:
Why this is acceptable: the image-only check raises the attack bar significantly. A naive
env > file.txtexfil is blocked. Encoding secrets into valid image bytes requires a sophisticated, multi-step attack that is unlikely from a prompt injection. And the fundamental truth remains: if you don't trust your agent's text output, you shouldn't trust its file output either — outbound attachments don't make the existing text-exfil risk worse.Flow
Changes
New modules:
src/outbound_rate.rs—HashMap<channel_key, VecDeque<Instant>>sliding-window rate limitersrc/media.rs::extract_outbound_attachments()— regex extraction + canonicalize + directory check + magic bytes + size/count validationsrc/media.rs::is_image_file()— magic bytes validation for PNG/JPEG/GIF/WebP/BMPModified:
src/config.rs—OutboundConfig(enabled, max_file_size_mb, max_per_message, max_per_minute_per_channel)src/adapter.rs—ChatAdapter::send_file_attachments(default no-op),AdapterRouterwiringsrc/discord.rs—DiscordAdapter::send_file_attachmentsvia serenityCreateAttachment::pathsrc/main.rs— module registration + config passthroughconfig.toml.example— documented[outbound]sectionSecurity Checklist (from #355)
std::fs::canonicalize()before directory check..)~/.oab/outgoing/(no configurable allowlist needed)OutboundConfig::default().enabled == falsePath::starts_withon canonical pathsConfig (opt-in)
Verified
cargo check✅cargo clippy✅ (non-test targets; pre-existingbool_assert_comparisonincron.rstests)cargo test— 190 passed, 0 failed (15 new: 10 outbound extraction + 5 rate limiter)Test coverage:
/tmp/pathsDiscord Discussion URL: https://discord.com/channels/1491295327620169908/1499016289086341250