Changed
Context
The issue agent cannot attach images to tasks. This came up adding a cropped mockup to Inventory #341: the crop was made locally, but nothing could get it onto the site.
- None of the six
contrib_taskstools creates media or files.task_updatealready passes<drupal-media>embeds through untouched, but only for media that already exists. drupal-contrib-liverunsdrush @hetz.contrib mcp:server issue_agenton the deploy host over SSH (scripts/mcp-server.sh), so a tool cannot read a file path from the client's machine. The bytes would have to travel in the JSON-RPC request.- The
issue_agentrole (config/contrib/user.role.issue_agent.yml) has no media permissions. - Image media on contrib: source field
field_media_image, extensionspng gif jpg jpeg, directory[date:custom:Y]-[date:custom:m], alt text required, no maximum file size. Thecontentformat's media embed defaults to thescreenshotview mode, so a bare embed renders at screenshot size. - Image media is named after its alt text by auto_entitylabel (
auto_entitylabel.settings.media.image), and filefield_paths renames the file after it on save. A name passed in is overwritten. tool_beltis in the codebase but not enabled on contrib. Its genericEntityCreatecannot carry file contents, so it would not solve this. It would also hand the agent generic tools that can delete entities, change user roles and send email.
Decisions
- A drush command, not an MCP tool. An agent cannot attach a file to a tool call: it would have to write out every base64 character of the image as its own output, which is impractical past a small crop. A drush command reads the image from STDIN instead, and the agent runs it from its shell:
drush @hetz.contrib contrib-tasks:upload-image mockup.png --alt="…" < mockup.png. Checked on 2026-10-23: 400 KB of binary piped intodrush @hetz.contribarrived byte for byte (matching sha256) in about two seconds, and drush runs aswww-datain the container, the same owner as browser uploads. - The command acts as
issue_agent. Drush runs as anonymous and entity saves check no access, so the command switches to the account (--as, defaultissue_agent) and checkscreateAccess('image')before saving. The role stays the whole policy, as for the tools, and the media is owned by the agent. - No
--nameoption. auto_entitylabel names the media after the alt text whatever is passed. - The embed is per site. The UUID it returns only resolves on the site it was uploaded to: upload with
@hetz.contribfor a task edited throughdrupal-contrib-live, with@ddev.contribfor the local copy. - No cleanup of orphans. If the upload works and the
task_updateafter it does not, the media stays. It is harmless and can be deleted at/admin/content/media.
TODO
- [x] Permission. Add
create image mediatoconfig/contrib/user.role.issue_agent.yml, withmedia.type.imageunder config dependencies andmediaunder module dependencies. Update the role list in the "The agent account" section of thecontrib_tasksREADME. - [x] Service.
ImageMediaUploaderincontrib_tasks: takes the bytes, a filename, alt text and the account. Refuses an extension the image field does not allow, a file over the field's limit or 5 MB when it has none, bytes that are not an image or do not match the extension, and empty alt text, each with a message saying why. Writes the file to the field's upload directory and creates animagemedia entity owned by the account. - [x] Command.
contrib-tasks:upload-image <filename> --alt= [--as=issue_agent], image on STDIN, JSON output by default: mediaid,uuid,name, the file URL and the embed string<drupal-media data-entity-type="media" data-entity-uuid="…"></drupal-media>. - [x] Kernel test for the service, on
ContribTasksKernelTestBase. - [x] Docs. The command in the
contrib_tasksREADME anddocs/sites/contrib.md; a line in the MCP server instructions (mcp_server.settings:server_instructions) and in therefine-issueskill: "to attach an image, run the drush command, then embed what it prints withtask_update". - [x] Embed in a paragraph.
Markdown::toHtml()wrapped a<drupal-media>line in<p>, which CKEditor never stores and which renders as empty paragraphs around the image. It now stores the embed on its own. - [x] Permission for the agent. Allow exactly
drush @hetz.contrib contrib-tasks:upload-imagein Claude Code, notdrush @hetz.*. - [x] Try it locally against
@ddev.contrib. - [x] Deploy: commit, deploy to live,
drush @hetz.contrib cim -yandcr. - [x] Reconnect:
/mcpand reconnectdrupal-contrib-live. The MCP server is one long-lived drush process, so until it restarts it runs the code it started with: heretask_updatekept wrapping embeds after the deploy. - [x] Repair #328 and #338. Earlier
task_updatecalls left their embeds wrapped in<p>on live. After the deploy, an update of each body through the tool stores them unwrapped.
Done when
drush @hetz.contrib contrib-tasks:upload-imagewith a PNG on STDIN and alt text returns a media UUID and an embed string; the media appears at/admin/content/media, owned byissue_agent.- That embed added to a task body with
task_updaterenders the image on the task page. - A file with a disallowed extension, an oversized file, bytes that are not an image, or a missing alt text is refused with a message saying why.
- The cropped Today panel mockup is embedded in Inventory #341.
Done
- 2026-10-23: built and tried locally against
@ddev.contrib: a PNG upload returned the embed, the media was owned byissue_agent, and the embed rendered in thescreenshotview mode. Missing alt text, a PNG named.jpg, empty STDIN and an unknown--asuser were each refused with a message. The test media was deleted afterwards.contrib_taskstests: 49 pass. phpcs and PHPStan are clean. - 2026-10-23, on live: the upload ran through the Claude Code allow rule without a prompt. A test PNG became media owned by
issue_agent, its file served, and it rendered in thescreenshotview mode; the test media was then deleted. Missing alt text, a PNG named.jpgand piped text were each refused with a message. - The Today panel mockup is media 119, embedded in #341's Context; the page shows it.
- After reconnecting
/mcp, #328, #338 and #341 were resaved throughtask_update. No task body on live holds<p><drupal-mediaany more, and each page still shows its image.
Related tasks
Task type
Project
Parent task
Topics
Modules