8.8 KiB
Matrix Shared Workspace File Flow Design
Goal
Bring the Matrix surface and platform-agent to a single file-handling model that matches the current platform runtime contract as closely as possible.
The result should be:
- Matrix receives user files and makes them visible to the agent through a shared
/workspace platform-agentreceives attachment paths, not ad hoc summaries or inline payloads- the agent can send files back to the user through the surface via
send_file - local development and the default deployment path use the same storage contract
Core Decision
The selected architecture is:
Matrix surface <-> shared /workspace <-> platform-agent
This means:
- the Matrix bot is responsible for downloading incoming Matrix media
- downloaded files are written into the same filesystem mounted into
platform-agent - the surface passes relative workspace paths to the agent as
attachments - the agent returns files to the user by emitting
MsgEventSendFile(path=...)
This is the current platform-native direction and does not require new platform endpoints.
Why This Decision
The current upstream platform changes already define the file contract:
MsgUserMessage.attachmentsislist[str]- each attachment is a path relative to
/workspace - the agent validates those paths against its configured backend root
- the agent can emit
send_file(path)back to the client
That is not an upload API and not a remote blob contract. It is an explicit shared-workspace contract.
Trying to preserve the current separate-process launch model would force the surface to fake production behavior with inline text extraction, out-of-band path rewriting, or a future upload API that does not exist yet. That would increase the gap between our runtime and the platform runtime instead of reducing it.
Scope
This design covers:
- shared workspace runtime for Matrix bot and
platform-agent - incoming Matrix file handling into shared storage
- attachment path propagation to
RealPlatformClientandAgentApi - outbound file delivery from agent to Matrix user
- local compose/dev workflow and README updates
This design does not cover:
- Telegram file flow
- encrypted Matrix media handling
- upload APIs on the platform side
- OCR, PDF parsing, or content extraction pipelines
- long-term object storage or file lifecycle policies beyond basic cleanup boundaries
Runtime Contract
Shared filesystem
Both containers must mount the same directory at /workspace.
Requirements:
- the Matrix bot can create files under
/workspace platform-agentsees the same files at the same relative paths- agent-originated files written under
/workspaceare readable by the Matrix bot
The contract is path-based, not URL-based.
Attachment path format
The surface sends attachments to the agent as relative workspace paths, for example:
surfaces/matrix/<matrix_user_id>/<room_id>/inbox/20260420-153000-report.pdfsurfaces/matrix/<matrix_user_id>/<room_id>/inbox/20260420-153200-photo.jpg
Rules:
- paths must be relative to
/workspace - paths must be normalized before sending to the agent
- surface-owned uploads must live under a dedicated namespace to avoid collisions with agent-created files
Data Flow
Incoming file from Matrix user
- Matrix receives
m.file,m.image,m.audio, orm.video. - The Matrix bot resolves the target room and platform chat context as usual.
- The Matrix bot downloads the media from Matrix.
- The file is stored under
/workspace/surfaces/matrix/.../inbox/.... - The outgoing platform call includes:
- original user text
attachments=[relative_path_1, ...]
platform-agentvalidates that those files exist and exposes them to the agent through the upstream attachment mechanism.
Important detail:
- the surface should not rewrite the user message into a synthetic file summary unless the message body is empty
- when body is empty, the surface may send a minimal synthetic text such as
User sent one or more attachments.
Outbound file from agent to Matrix user
- The agent uses
send_file(path). platform-agentemitsMsgEventSendFile(path=...).- The Matrix integration catches that event.
- The Matrix bot resolves the file inside shared
/workspace. - The Matrix bot uploads the file to Matrix and sends the appropriate media message to the room.
Surface behavior:
- if MIME type and extension are known, send the closest native Matrix media type
- otherwise send as
m.file - user-visible failures must be explicit if the referenced file does not exist or cannot be uploaded
Filesystem Layout
The Matrix surface owns a dedicated subtree:
/workspace/
surfaces/
matrix/
<sanitized-user-id>/
<sanitized-room-id>/
inbox/
20260420-153000-report.pdf
Design constraints:
- sanitize user ids and room ids before using them as path components
- preserve the original filename in the final basename where possible
- prefix filenames with a timestamp or unique id to avoid collisions
This layout is intentionally surface-scoped. The agent may read these files, but the surface remains the owner of how inbound messenger files are organized.
Components
Matrix attachment storage helper
Add a focused helper module responsible for:
- building stable workspace-relative paths
- sanitizing path components
- downloading Matrix media into
/workspace - returning attachment metadata needed by the platform layer
This helper should not know about agent transport details beyond the final relative path output.
Real platform client
RealPlatformClient must pass attachment relative paths through to AgentApi.send_message(...).
It must also surface non-text agent events needed by the Matrix adapter, especially MsgEventSendFile.
Agent API wrapper
AgentApiWrapper must be compatible with the modern upstream protocol:
/v1/agent_ws/{chat_id}/attachmentson outgoing user messagesMsgEventToolCallChunkMsgEventToolResultMsgEventCustomUpdateMsgEventSendFileMsgEventEnd
Matrix bot outbound renderer
The Matrix adapter must support sending files back to the room.
At minimum it needs:
- path resolution inside shared workspace
- Matrix upload of the local file
- send of an
m.fileor native media event with filename and MIME type
Deployment Changes
Compose
The repository root docker-compose.yml becomes the primary prod-like local runtime.
It should define at least:
matrix-botplatform-agent- one shared volume mounted as
/workspaceinto both services
The default developer workflow should stop describing platform-agent as a separately started side process.
Environment
The Matrix bot must connect to the in-compose platform-agent service by service name, not by assuming a separately launched localhost process.
The agent WebSocket configuration in docs and examples must match the modern upstream route.
Error Handling
Incoming files
If the Matrix bot cannot download or persist the file:
- do not send a broken attachment path to the agent
- return a user-visible error in the room
- log the Matrix event id, room id, and failure reason
Outbound files
If the agent asks to send a missing file:
- log a structured warning with the requested path
- send a user-visible message that the file could not be delivered
Shared workspace mismatch
If the runtime is misconfigured and /workspace is not actually shared:
- inbound attachments will fail agent-side path validation
- outbound
send_filewill fail surface-side file resolution
The implementation should make such failures obvious in logs rather than silently degrading to text-only behavior.
Testing
The implementation must cover:
- Matrix media download writes into the expected workspace-relative path
RealPlatformClientforwards attachment relative paths to the agent API- Matrix plain messages with attachments preserve the original text while adding attachment paths
- empty-body attachment-only messages produce the synthetic text fallback
AgentApiWrapperacceptsMsgEventSendFilewithout treating it as unknown- Matrix outbound file handling converts
MsgEventSendFileinto a Matrix upload/send call - compose configuration mounts the same workspace into both containers
Non-Goals
- no inline text extraction MVP
- no temporary URL-passing contract to the agent
- no fake “prod” mode with separate local filesystems
- no platform API additions in this phase
Success Criteria
- the default local runtime uses a shared
/workspace - a user can send a file in Matrix and the agent receives it through upstream
attachments - the agent can emit
send_file(path)and the Matrix user receives the file in the same room - our runtime behavior matches the current platform contract closely enough that moving from local compose to production does not require redesigning file flow