feat: outbound image/file attachments from agent → Discord#300
feat: outbound image/file attachments from agent → Discord#300DrVictorChen wants to merge 2 commits intoopenabdev:mainfrom
Conversation
Add extract_outbound_attachments() to detect  markers in agent responses and upload matching files via CreateAttachment. Security: path allowlist (/tmp/, /var/folders/), 25MB size cap. Includes unit tests for extraction, blocking, and edge cases. Ref: openabdev#298 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
macOS 26.3+ AMFI enforcement causes cargo-built adhoc-signed binaries to hang at _dyld_start when launched outside the original build session. `make build` and `make install` auto-run `codesign --force --sign -` on Darwin to prevent this. No-op on Linux. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Thank you for this well-researched PR — the prior art analysis and cross-agent testing are especially thorough. After reviewing the implementation, we've decided not to move forward with this feature at this stage. Opening a direct path from the local filesystem to a public Discord channel introduces security surface area (path traversal, symlink following, unintended data exfiltration) that we're not comfortable shipping right now. We appreciate the effort and the detailed write-up. If there's strong community interest, we'd love to revisit this in the future — upvotes on the linked issue (#298) are welcome and will help us prioritize. |
masami-agent
left a comment
There was a problem hiding this comment.
Agreeing with chaodu-agent's assessment — the security surface area is the right concern to prioritize here.
That said, this is a real user need (agents have no native path to send files back), and the research in this PR is valuable. Here's a suggested path forward:
Security hardening needed before this can ship
-
Symlink resolution —
std::fs::metadata()follows symlinks, soln -s /etc/passwd /tmp/innocent.txtbypasses the allowlist. Fix: usestd::fs::canonicalize()before checking the prefix:let canonical = std::fs::canonicalize(&path).ok()?; let allowed = OUTBOUND_ALLOWED_PREFIXES .iter() .any(|prefix| canonical.starts_with(prefix));
-
Path traversal —
could bypass prefix checks.canonicalize()also resolves..components, so fix #1 covers this too. -
Make allowlist configurable — Hardcoded
/tmp/and/var/folders/works for local dev but not for containerized deployments where agents write to/home/agent/output/or similar. Addoutbound.allowed_dirstoconfig.toml. -
Opt-in, not opt-on — This feature should be disabled by default and require explicit
outbound.enabled = truein config. Operators should consciously decide to allow filesystem → Discord uploads. -
Rate limiting — Consider a per-message or per-minute cap on outbound attachments to prevent an agent from flooding a channel.
Suggested next steps
- Open a follow-up issue capturing these security requirements
- The core implementation (regex extraction,
CreateAttachment, marker stripping, tests) is solid and can be reused once the security layer is in place - The Makefile change (macOS codesign fix) is unrelated and should be a separate PR — it's useful on its own
@DrVictorChen — thank you for the thorough research and cross-agent testing. The prior art analysis (OpenClaw CVE, Hermes fallback chain) directly informed the security concerns above. Would you be interested in opening a follow-up issue and iterating on the security hardening?
|
Hi @DrVictorChen, We've opened a follow-up issue #355 that captures the security requirements for this feature. The core implementation in this PR is solid — it just needs the security hardening layer before it can ship. When you're ready, you can either:
The key changes needed:
Looking forward to the next iteration! Let us know if you have any questions. |
What problem does this solve?
Agents running through OpenAB can receive images from users (PR #158), but have no native path to send images/files back. The only workaround is agents calling the Discord REST API directly via
curlwith the bot token — requiring the agent to know the token and channel ID, sending messages outside OpenAB's thread context, and breaking portability across agents.Closes #298
At a Glance
Prior Art & Industry Research
OpenClaw:
Uses a
MEDIA: /path/to/filetext directive pattern. The agent writesMEDIA: <path-or-url>inline in its response text. The gateway'ssplitMediaFromOutput()(src/media/parse.ts) extracts these via regex, then routes through a 4-stage pipeline: parse → load (with SSRF protection, HEIC conversion, size capping) → normalize → channel delivery. Each channel plugin implementssendPhoto/sendDocument/etc.Key lessons from OpenClaw:
MEDIA:appearing in tool results or docs triggers unintended file loading (#18780, #16935)mediaLocalRootsdirectory allowlistHermes Agent:
Uses a nearly identical
MEDIA:/path/to/filetag convention.BasePlatformAdapter.extract_media()(gateway/platforms/base.py) parses output, returns(media_files_list, cleaned_text). Tags are stripped from displayed text. File routing is extension-based:.png/.jpg→send_image_file(),.ogg/.mp3→send_voice(),.mp4→send_video(), everything else →send_document(). Each platform adapter overrides the methods it supports, with graceful fallback to text.Key lessons from Hermes:
send_imagetools — they embedMEDIA:in text and rely on post-processing (#4701)Other references — the agent runtimes we tested with:
OpenAB bridges CLI agents to Discord via ACP (Agent Client Protocol). The four agents we tested are:
@agentclientprotocol/claude-agent-acp)when referencing files. Can generate images via tools and reference them in output.All four agents naturally output
when referencing local files — this is standard LLM markdown behavior, which is why we chose markdown syntax over a customMEDIA:directive.Comparison table:
MEDIA: /pathMEDIA:/path(markdown)mediaLocalRootsallowlist (post-CVE)/tmp/,/var/folders/allowlistMEDIA:in text)![]()is structural)Proposed Solution
Outbound Attachment Detection & Upload
is intercepted byextract_outbound_attachments()indiscord.rs/tmp/or/var/folders/, file must exist, be a regular file, and ≤ 25MBCreateAttachment::path()+CreateMessage::new().add_file()macOS Build Fix (Makefile)
cargo buildon macOS 26.3+ produces adhoc-signed binaries that hang at_dyld_startdue to AMFI enforcement.make build/make installauto-runcodesign --force --sign -on Darwin. No-op on Linux.Why this approach?
We chose markdown image syntax (
) overMEDIA:for three reasons:Lower false positive risk — Both OpenClaw and Hermes suffer from
MEDIA:appearing in tool outputs, documentation, or release notes and triggering unintended file loads. Markdown![]()syntax is structurally distinct and rarely appears in agent prose outside of intentional use.Natural for all 4 tested agent runtimes — Claude Code, Codex, Cursor, and Copilot all produce markdown
when referencing images. No special prompting needed — the agents already know this syntax.Minimal code change — OpenAB is Discord-only, so we don't need the multi-platform routing complexity of OpenClaw (4-stage pipeline) or Hermes (per-platform adapter chain). A single regex +
CreateAttachmentcovers the use case in ~70 lines of Rust.The path allowlist approach was directly informed by OpenClaw's CVE (GHSA-r8g4-86fx-92mq) — we enforce it from day one rather than retrofitting after a security incident.
Alternatives Considered
MEDIA:directive (OpenClaw/Hermes style)<attachment path="..." />EditMessagewith attachmentEditMessagedoesn't support adding attachments to existing messages; must useCreateMessagefor follow-upValidation
cargo checkpassescargo test— 39/39 pass, including 5 new outbound attachment tests:outbound_no_markers_passthrough— text without markers unchangedoutbound_extracts_tmp_file—/tmp/file correctly extractedoutbound_blocks_non_allowlisted_path—/etc/passwdblockedoutbound_ignores_nonexistent_file— missing file kept as textoutbound_handles_multiple_attachments— multiple files in one response@agentclientprotocol/claude-agent-acp)Observation from live testing:
Some agents (notably Codex and Copilot) attempted to bypass the native mechanism by calling the Discord REST API directly via
curlwith the bot token. This confirms the original problem statement — agents resort to hacky workarounds when no native path exists. Notably, even when an agent used thecurlworkaround, OpenAB's outbound handler still triggered simultaneously on themarkers in the same response, successfully uploading the file through the native path. After the agents discovered the native mechanism (by readingdiscord.rs), they switched to usingexclusively.Log output confirming successful upload:
🤖 Generated with Claude Code