Skip to content

Inbound WhatsApp media (images/docs/audio) is unreachable by the agent. Files save to unmounted DATA_DIR/attachments instead of the session inbox #2715

@jon-ruth

Description

@jon-ruth

Summary

On v2, inbound WhatsApp attachments are downloaded to a host directory that is not mounted into the agent container, and the agent is handed a /workspace/attachments/... path that doesn't exist inside the container. As a result the agent cannot open images/documents/audio a user sends — it reports the file as missing.

Environment

  • NanoClaw v2.0.76
  • WhatsApp channel installed via /add-whatsapp (from the channels branch), Baileys 7.0.0-rc.9
  • Runtime: Docker, macOS host

Steps to reproduce

  1. Install the WhatsApp channel and wire it to an agent group.
  2. Send the bot an image (or PDF / voice note) in a DM.
  3. Ask the agent to describe / read the attachment.

Expected

The agent can read the file (e.g. via the Read tool / multimodal), since v2 stages inbound attachments into the session inbox that's mounted at /workspace.

Actual

The agent reports it can't find the file. The attachment never reaches a path inside the container.

Root cause

downloadInboundMedia() in src/channels/whatsapp.ts:

  • writes the downloaded buffer to path.join(DATA_DIR, 'attachments'), and
  • returns the attachment as { type, name, localPath: 'attachments/<file>' } (no base64 data).

Two problems compound:

  1. DATA_DIR/attachments is never mounted. The container's /workspace is mounted from the per-session directory (sessionDir(...)), not from DATA_DIR. So the /workspace/attachments/... path the formatter surfaces to the agent doesn't exist in the container.
  2. extractAttachmentFiles() in src/session-manager.ts only stages attachments that carry base64 data (if (typeof att.data !== 'string') continue;). Because the WhatsApp adapter emits a localPath and no data, the staging step skips it entirely — so the media is never copied into the mounted inbox/<messageId>/.

Net: the adapter bypasses v2's own attachment-staging contract (base64 data → host stages into the session inbox → rewrites a valid in-container localPath), which the tests in src/host-core.test.ts exercise with { name, data, size }.

Proposed fix

Have downloadInboundMedia() return base64 data and let the host's extractAttachmentFiles() do the staging (which also re-sanitizes the filename and writes safely with the wx flag). No pre-write to DATA_DIR needed:

// src/channels/whatsapp.ts — downloadInboundMedia()
const buffer = (await downloadMediaMessage(msg, 'buffer', {})) as Buffer;
const rawFilename = normalized[key].fileName;
const fallback = `${type}-${Date.now()}${ext}`;
const filename = isSafeAttachmentName(rawFilename) ? rawFilename : fallback;
results.push({
  type,
  name: filename,
  data: buffer.toString('base64'),
  size: buffer.length,
});

(Drop the DATA_DIR/attachments mkdirSync / writeFileSync / localPath branch; the DATA_DIR import then becomes unused.)

Side benefits

  • Routes through the existing inbox staging, which is per-session (proper isolation — no shared DATA_DIR/attachments visible across agent groups) and already hardened against symlink / path-traversal attacks.
  • Brings the WhatsApp adapter in line with the documented attachment contract and the host-core tests.

Verified locally: with this change, inbound images / PDFs are staged into inbox/<messageId>/ and the agent reads them normally.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions