Skip to content

Attachments and S3-compatible storage

HoneyDew supports original file attachments on issues and Epics, managed-object APIs, Python helpers, and an optional stdio MCP server. Local storage works without a cloud account.

Use the UI

Open a card or Epic and choose Add files, drop files into Attachments, or paste an image into the drop area. Files selected on a new card upload after Create succeeds; a failed save preserves the draft. Uploads show real progress, cancellation and retry. Rename changes the display name only. Download returns original bytes; Preview image generates a safe PNG thumbnail, never renders uploaded HTML/SVG. Use Remove and confirm to detach.

Choose Attachment storage above the board for Local filesystem or S3-compatible settings. Defaults are 25 MiB/file, 100 links/card, four concurrent uploads/process and a 24-hour unreferenced-attachment grace period. File size/link limits are editable in the UI; the settings API also supports a total-byte quota and retention hours.

Configuration and persistence

The first startup creates a local location at <project-root>/data/attachments. Set HONEYDEW_STORAGE_ROOT before the first startup to choose another initial directory; subsequent settings are stored transactionally in SQLite. Settings do not rewrite config.json. HONEYDEW_DATABASE_URL optionally selects an isolated SQLite database; otherwise the existing sqlite:///./kanban.db convention is unchanged.

GET /api/storage returns non-secret settings, revision, limits and retained locations. PUT /api/storage takes revision, location and limits; a stale revision gets 409. Each successful save preserves immutable location records; changing defaults affects new uploads only. Existing objects continue to use their original location. Referenced and historical locations cannot be removed. GET /api/config never includes storage credentials or private provider settings.

S3-compatible connection

Supply an existing bucket, optional endpoint origin, region, dedicated prefix, addressing style (auto, path, virtual), and optional host credential profile name. The backend uses boto3's host-owned AWS credential provider chain; configure credentials through that provider's secure setup, not chat, task descriptions, API payloads or browser fields. Empty profile uses the default server provider. The UI indicates host-managed credentials and verifies access through Test connection; it never displays credential values.

TLS verification is enabled. An explicit development checkbox permits HTTP for a selected local test service. No insecure TLS-verification bypass is provided. Testing and saving each perform a tiny put/get/head/delete probe; they do not create buckets. Grant the configured prefix only object put/get/delete access (HEAD uses read access); ordinary transfers do not require bucket listing. Use a private bucket. S3 failures return 503, never silently fall back to local disk.

Verified compatibility: boto3's basic object operations over HTTP against a disposable Moto S3-compatible service. AWS S3, MinIO, R2 and other hosted vendors must be verified against their actual configurations before claiming vendor-specific support. No paid-provider account is connected by installation.

REST APIs

Send one multipart file and explicit configured profile for uploads. Do not force a JSON Content-Type. Use an Idempotency-Key to safely retry the same file and metadata. A completed replay returns 200 and the same object; fresh upload returns 201; mismatched key/content returns 409. Failed unpublished uploads can retry with the same key without losing cleanup records. An in-progress upload returns 409; wait and retry. A deliberately removed successful attachment is not recreated by replay.

Method Path Behavior
POST /api/objects Upload retained standalone object
GET /api/objects/{id} Object metadata/state
GET / HEAD /api/objects/{id}/content Original bytes / headers
DELETE /api/objects/{id} Queue unlinked object deletion; linked objects get 409
GET /api/cards/{card_id}/attachments?offset=0&limit=100 {items,total,offset,limit} metadata page
POST /api/cards/{card_id}/attachments Upload and attach; same endpoint for issues and Epics
POST /api/cards/{card_id}/attachments/from-object JSON {object_id,profile,filename?}
GET /api/cards/{card_id}/attachments/{id} Link/object metadata
GET / HEAD /api/cards/{card_id}/attachments/{id}/content Original bytes / headers; optional preview=true safe image
PATCH /api/cards/{card_id}/attachments/{id} JSON {filename}
DELETE /api/cards/{card_id}/attachments/{id} Idempotent detach (204)
GET / PUT /api/storage Read / revision-checked non-secret settings
POST /api/storage/test Explicit candidate location probe
DELETE /api/storage/locations/{id} Remove unused nondefault location only

Responses include IDs, filename, MIME, byte size, SHA-256, creator/time and relative content_url. Prefix links with the API origin when using a remote host. Download headers use attachment disposition, nosniff, no-store and SHA-256. No raw provider credentials/keys are used as public file addresses. Upload request/body caps also apply to chunked requests. 413 means file/request too large; 429 means quota/concurrency admission rejected; 422 means invalid metadata; 404 means absent object/link; 503 means provider unavailable. Listings include metadata, never eager binary payloads. Board/card summaries expose attachment_count.

Python and remote agents

Run from the repository root with backend dependencies installed:

from tools.kanban_tools import KanbanTools

with KanbanTools() as api:  # SMARTIFY_API_URL or localhost:8000
    result = api.upload_attachment(card_id, "artifact.pdf", profile="umbra",
                                   idempotency_key="artifact-attempt-1")
    api.download_attachment(card_id, result["id"], "downloaded-artifact.pdf")

Use the actual configured profile ID, not a guessed owner. Helpers also provide put_object, get_object, download_object, delete_object, attach_object, list_attachments, get_attachment, rename_attachment, delete_attachment and get_storage_status. Downloads verify bytes/SHA-256 and refuse to overwrite. upload_stream accepts a binary file-like stream for agents whose artifacts are already in memory or on another host. Python paths always refer to the machine running Python. The HTTP API never reads server files from a caller-supplied path.

Native MCP setup

Install the optional MCP dependency set with the repository virtual environment, then run the module from the repo root:

.venv/bin/pip install -r tools/requirements-mcp.txt
.venv/bin/python -m tools.mcp_server

Configure the host to launch that command with cwd set to the HoneyDew checkout. Non-secret environment settings:

  • SMARTIFY_API_URL: reachable HoneyDew API origin.
  • HONEYDEW_MCP_READ_ROOTS: explicitly permitted source directories, separated by the OS path separator.
  • HONEYDEW_MCP_WRITE_ROOTS: explicitly permitted destination directories, separated by the OS path separator.

File operations are disabled unless roots are configured. Paths belong to the MCP server host, not necessarily the agent host or API host. Resolved containment, no-follow descriptor operations, regular-file checks and atomic no-overwrite downloads enforce these limits. Use REST/Python byte uploads when the file exists on a different host.

MCP tools: get_storage_status, list_attachments, get_attachment, upload_attachment, download_attachment, rename_attachment, delete_attachment, attach_object, put_object, get_object, download_object, delete_object. The resource template honeydew://cards/{card_id}/attachments/{attachment_id} returns fresh metadata. Inspect/list first, download only needed files, and keep bulk binary data out of model context. Storage status omits host paths; no MCP tool accepts provider credentials.

Retention and recovery

Archive keeps attachments. Transfers and board moves preserve them. Epic deletion retains its child cards' attachments. Card/column/board deletion cascades links but preserves independent object locators. Standalone objects remain until explicit object deletion; attachment-owned bytes are eligible for deletion after the last link disappears and the grace period elapses.

Maintenance checks every 60 seconds. StoredObject doubles as the durable cleanup job (state, locator, attempts, next retry, sanitized error), rather than a separate queue table. GC atomically claims objects before provider I/O; relinking is blocked during deletion. Provider failures retry with backoff; an hour-old interrupted upload expires; stale managed local staging files are also removed. No bucket-wide scan or removal of unrelated keys occurs.

A consistent backup includes metadata and every ready blob, including S3 bytes. Run during an idle window: backup holds a SQLite write lock while copying blobs, so reads continue but writes may wait or time out.

.venv/bin/python -m tools.storage_backup backup --database backend/kanban.db --output /path/to/new-backup
.venv/bin/python -m tools.storage_backup restore --backup /path/to/new-backup --output /path/to/new-restore

Both output directories must be new. Each blob is SHA-256/size verified. Restore remaps all object states to its isolated local folder and rewrites the default there: restored cleanup cannot touch the original S3 bucket or filesystem. Launch against the restored kanban.db with a matching copy of the original non-secret config.json so startup board reconciliation does not prune restored boards. Verify isolated downloads before switching deployments. No automatic migration is implemented; backup/restore provides a verified offline route to a new local installation.

Rollback: retain the backup, metadata tables and blob roots. Revert application code/config only after disabling writes; do not drop attachment data. Older versions do not maintain these object records, so avoid hard card/board deletions or config-driven board pruning until an attachment-aware release is restored.

Development verification

Install backend dev and optional integration dependencies, then run:

.venv/bin/pip install -r backend/requirements-dev.txt -r tools/requirements-mcp.txt
PYTHONPATH=backend:. .venv/bin/python -m pytest backend/tests
npm --prefix frontend test
npm --prefix frontend run lint
npm --prefix frontend run build
npm --prefix frontend exec playwright install chromium
npm --prefix frontend run test:e2e

Browser tests start disposable API/SQLite/blob storage on port 8012 and Vite on 5182; they never use production data. Python protocol tests launch ephemeral HTTP/MCP processes and a disposable S3-compatible server. Hosted vendor tests require separately configured approved test storage.

Embedded MCP

The backend also serves Streamable HTTP MCP at /mcp/ automatically. It includes task-management tools and attachment metadata/link/rename/delete operations, but deliberately excludes server-file-path transfers and storage configuration. Use REST byte endpoints or the local stdio tools above for file transfer. See Connect.