Skip to content

feat(channels/discord): outbound attachments with SSRF-guarded multipart batching#1229

Open
benhoverter wants to merge 1 commit into
RightNow-AI:mainfrom
benhoverter:topic/discord-outbound-attachments
Open

feat(channels/discord): outbound attachments with SSRF-guarded multipart batching#1229
benhoverter wants to merge 1 commit into
RightNow-AI:mainfrom
benhoverter:topic/discord-outbound-attachments

Conversation

@benhoverter

Copy link
Copy Markdown
Contributor

Summary

Lets agents send images and files outbound through the Discord channel.
This is a deliberately narrowed re-submission of the transport half of #1162
(closed 2026-05-12) — see "How this addresses the #1162 review" below.

Scope is exactly one file of production code (crates/openfang-channels/src/discord.rs):

  • New Multipart arm batches N attachments into a single Discord message via
    multipart/form-data, with greedy byte-cap packing for Discord's 25 MiB / 10-file
    limits.
  • Handles ChannelContent::FileData (pre-resolved bytes), and File / Image
    blocks carrying URLs.
  • SSRF guard on every URL fetch: scheme allowlist, literal-IP host check
    (loopback, RFC1918, link-local, unique-local, multicast, unspecified, CGNAT),
    IPv4-mapped IPv6, and redirect revalidation at each hop (URL_FETCH_MAX_REDIRECTS).
  • Streaming download with a hard 25 MiB size-cap and mid-stream abort; 15 s timeout.
  • Body-aware 429 retry-after honored on the multi-file path; partial-send failure
    emits a structured, grep-friendly WARN.
  • Fetcher trait extracted so tests can stub the wire without disabling the guard.

How this addresses the #1162 review

#1162 was closed for two blockers. This branch resolves both.

/cc @jaberjaber23 — you reviewed the original #1162; flagging so you can see both
blockers you raised are addressed below.

  1. Credential-exfil via too-wide allow_roots — the blocking concern. Not
    present in this branch.
    This PR contains no <openfang:attach path=…> parser,
    no default_allow_roots/allow_root logic, and no local filesystem reads at all

    (fs::read / File::open / tokio::fs — none). It transports pre-resolved bytes
    (FileData) and SSRF-guarded URLs only. The local-path attach surface that the
    reviewer flagged is intentionally out of scope here and will land separately
    with allow_roots empty-by-default + a hard ~/.openfang/ refusal, as requested.
    This matches the reviewer's own suggested split ("PR A: smaller, easier review").

  2. Dirty against main (fix(channels/discord): surface image attachments to text-only providers #1143/feat(channels): harden channel_id binding — adapter allowlist, strict validation, single source of truth for routing #1147/runtime/claude_code: materialize image blocks to tmpfile + extract image_cache module #1151/Unintended behavior of OpenFang workspaces #1097 drift) — resolved. Branch is
    rebased on current upstream/main; git merge-tree is clean. It builds on the
    #1143 Multipart types already in main (no duplicate variant) and carries
    no parallel image_cache implementation.

Out of scope (intentional)

Test plan

Tests live in-file (discord.rs test module):

  • SSRF guard: blocks loopback, 10/192.168, link-local, non-HTTP scheme, [::1],
    IPv4-mapped metadata ([::ffff:169.254.169.254]); allows public IP literal;
    asserts errors never leak the query string.
  • Multipart: greedy byte-cap chunking, empty/whitespace-only caption handling,
    mixed Text/Image/File dispatch, empty-multipart rejection, body-aware 429 on the
    aggregated path, SSRF-blocked File URL surfaces send() -> Err.
  • Fixture servers bound to 127.0.0.1 use an explicit test-only ssrf_bypass
    fetcher so the production guard stays enforced everywhere else.

Run before submit: cargo test -p openfang-channels, cargo clippy -p openfang-channels --all-targets -- -D warnings.

Note for review

The Fetcher trait + SSRF guard is the security-sensitive surface — flagging it for
closer review (same as #1162; that engineering was praised, only the path-attach
allow-root sank the prior PR, and it isn't here).

…art batching

Squashed reroll of topic/discord-outbound-attachments onto v0.6.9 (acf2587).
Replaces the 16-commit history that landed via local-main merge 2c32102.

Reroll context:
  - mime/size on ChannelContent::File and file:// in download_image_to_blocks
    already landed upstream via RightNow-AI#1143; dropped from this branch.
  - source_url-on-ContentBlock::Image is provided by feat/runtime-image-cache
    (PR RightNow-AI#1151); the three compactor source_url tests live there.
  - Net contribution here is the outbound Multipart feature plus its deps.

Feature scope:
  - Outbound for ChannelContent::File and Image URL arms (per-block resolver
    dispatch in mixed Multipart payloads).
  - N-attachment batching with greedy-pack chunker and aggregate per-chunk
    byte cap; parallel attachment fetch via try_join_all.
  - SSRF guard + redirect revalidation + log scrubbing for URL fetch
    (Fetcher trait abstraction).
  - Body-aware 429 retry on the multi-file path; structured partial-send
    WARN with grep-friendly event.
  - Transparent decompression disabled on image download (bridge.rs).

Verification:
  - cargo check --workspace: clean
  - cargo test -p openfang-channels -p openfang-runtime: 1002/1002

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant