Hand documents to a worker and bring its deliverables back through native Kanban task attachments, without moving files by hand or reading a worker path.
A coordinator (for example a first officer) can:
- upload an input file to a task,
- list the artifact references a task carries, and
- download the exact bytes of one referenced revision.
Hermes Helmet adds no service, artifact database, queue, or scheduler. The task's
native attachments remain the only store, and the deployment's existing
worker-runtime transport (HERMES_HELMET_WORKER_RUNTIME) carries the bytes.
helmet artifact upload BOARD TASK_ID ./brief.docx
helmet artifact list BOARD TASK_ID [--role original|preview|extracted-text]
helmet artifact download REFERENCE ./retrieved.docx [--board BOARD] [--task-id TASK_ID]Each command prints JSON describing the artifact and never prints file contents.
Pass --worker-runtime to override the environment variable.
Every listed artifact carries the identity and exact revision a later review needs:
| Field | Meaning |
|---|---|
reference |
kanban-artifact:v1:BOARD:TASK:ATTACHMENT_ID:SHA256 |
board, task_id |
Owning board and task |
attachment_id |
The native attachment id (reused, not reissued) |
filename, media_type, size |
Stored name, stated media type, bytes |
sha256 |
Checksum of the exact revision |
role, companion_of |
original, preview, or extracted-text, and the original's name |
uploaded_by, created_at |
Native provenance |
The reference is the only thing that names an artifact. It is not a file path,
and the native stored_path is a worker location the coordinator cannot read, so
it is dropped from every result. Local paths, URLs, other tasks' attachments, and
bare numbers are rejected with artifact_ref_malformed, artifact_task_mismatch,
or artifact_board_mismatch; they cannot stand in for a task-owned reference.
The checksum is the revision. A download succeeds only if the task still lists the
attachment with the checksum in the reference, and only if the received bytes match
the listed size and SHA-256. A changed or replaced file fails with
artifact_revision_mismatch, so feedback and acceptance cannot silently apply to
a superseded revision. Upload never replaces an attachment: a new revision is a
new attachment with a new id, and earlier revisions stay retrievable.
The original DOCX or PPTX is the editable artifact and is stored byte-for-byte. A
rendered preview or extracted text is a separate attachment named after its
original (brief.docx.preview.png, brief.docx.extracted.txt), uploaded with
--role preview|extracted-text --original REFERENCE. A companion never replaces
or stands in for its original, and --role original listings exclude it. Those
two name suffixes are reserved for companions.
- Files are limited to 25 MiB, the Kanban runtime's per-file cap. Oversize input is refused before anything is sent, and an adapter that still receives too much answers with exit status 4.
- Bytes travel on the adapter process's stdin and stdout, never as arguments and never as base64 in a prompt. The adapter's stderr is discarded.
- Failures are fixed codes with a fixed hint (
err:CODE: hint). They never echo file contents, adapter output, or credentials.
| Code | Meaning |
|---|---|
worker_runtime_unset |
No adapter configured |
artifact_ref_malformed |
Not a kanban-artifact:v1: reference |
artifact_board_mismatch, artifact_task_mismatch |
Reference or record belongs elsewhere |
artifact_not_found |
Task or attachment absent (also adapter exit 3) |
artifact_revision_mismatch |
Attachment no longer has the referenced checksum |
artifact_integrity_failed |
Received bytes differ from the listed size or SHA-256 (also exit 5); nothing is kept |
artifact_checksum_missing |
The adapter listed an artifact without a SHA-256 |
artifact_too_large |
Over 25 MiB (also exit 4) |
artifact_source_invalid |
Upload source is not an existing regular local file |
artifact_name_invalid, artifact_name_reserved |
Unsafe name, or a companion-only name |
artifact_media_type_invalid |
Not type/subtype |
artifact_role_invalid, artifact_original_required, artifact_original_invalid, artifact_companion_exists |
Companion misuse |
artifact_metadata_mismatch |
The adapter did not preserve the media type, size, checksum or name |
artifact_destination_invalid, artifact_destination_exists |
Unsafe or occupied download target |
artifact_transport_unavailable, artifact_transport_timeout, artifact_transport_failed, artifact_transport_invalid |
The adapter could not run, timed out, failed, or broke this contract |
A download is written to a new file atomically. It never follows a symlink and
does not overwrite an existing file unless --overwrite is given. Without
--overwrite the guarantee holds at publication, not only before the transfer: the
verified bytes become visible only if nothing exists at the destination at that
moment, so a file another process creates while the artifact is being fetched is
kept and the download fails with artifact_destination_exists.
The artifact_transport_timeout deadline covers the whole local invocation. The
adapter runs in its own process group; when it finishes, overflows, or times out,
that group is killed, so a wrapper's child (for example a container exec) cannot
outlive the call or keep the output pipe open. A process that deliberately leaves
the group cannot be killed from here, but it still cannot hold the call past the
deadline plus a short cleanup allowance.
The adapter owns authentication, the Captain/worker boundary, and the native
attachment calls. It needs exactly three verbs. RUNTIME is the configured
command prefix. Hermes Helmet ships one reference adapter for a local Docker
Compose worker, helmet-compose; a
deployment with its own topology, identities, or other runtime verbs writes its
own adapter to this contract, and may delegate these three verbs to the public
helper instead of reimplementing them.
| Invocation | stdin | stdout |
|---|---|---|
RUNTIME artifact-upload BOARD TASK --name N --media-type M --size S --sha256 H --json |
the exact bytes | one JSON attachment record |
RUNTIME artifact-list BOARD TASK --json |
none | a JSON array of attachment records |
RUNTIME artifact-download BOARD TASK ATTACHMENT_ID |
none | the exact bytes and nothing else |
An attachment record is the native attachment row plus the checksum:
{
"id": 41,
"task_id": "t_0123abcd",
"filename": "brief.docx",
"content_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"size": 18432,
"sha256": "<64 lowercase hex characters>",
"uploaded_by": "coordinator",
"created_at": 1760000000,
"stored_path": "<worker location; accepted and ignored>"
}The adapter must:
- store uploads through the native attachment write path so the 25 MiB cap, name sanitization, durable storage, and attachment id are the runtime's own;
- verify the declared size and SHA-256 before storing (exit 5 on mismatch) and
compute
sha256over the stored blob when listing; - scope every lookup to
BOARDandTASK; the same attachment id on another task is not found (exit 3); - exit
0on success,3not found,4over the cap,5integrity mismatch, and any other status for a failed transport (126and127, not executable or command not found, report it as unavailable); - keep credentials out of stdout, and not rely on stderr being surfaced.
The coordinator verifies every record and every download again, so a faulty adapter fails closed instead of delivering the wrong bytes.
helmet-compose is the public reference adapter for a native Hermes worker in a
dedicated Docker Compose project. It is a single console entrypoint
(hermes_helmet.compose_runtime), installed with the package, and it plays two
roles:
export HERMES_HELMET_HERMES=helmet-compose # native `hermes kanban ...`
export HERMES_HELMET_WORKER_RUNTIME=helmet-compose # artifact-upload|list|download| Supported | Behavior |
|---|---|
helmet-compose kanban ... |
The container's native hermes kanban ..., arguments unchanged |
helmet-compose artifact-upload|artifact-list|artifact-download BOARD TASK ... |
The adapter contract above, served by hermes_helmet.native_artifacts |
ledger-root, ledger-watch, dispatch-root, prepare-repo, wait, and anything else |
Refused with exit 2 and unsupported_verb; Docker is not called |
- Explicit target.
COMPOSE_FILEandCOMPOSE_PROJECT_NAMEmust both be set. Neither has a default, so the adapter never falls back to another project. They are Docker-wide variables: everydocker composecommand in that shell targets the same project. Use a dedicated shell for the worker project, do not export them from a shell startup file, and close the shell when finished. SetHERMES_HELMET_HERMESandHERMES_HELMET_WORKER_RUNTIMEin that shell only so an existing general worker-runtime configuration is not replaced. - Transport. Each call is
docker compose -p PROJECT exec -T hermes ...into the service namedhermes, with the argument vector passed without a shell. The adapter then hands the process to Docker, so stdin and stdout stay the caller's binary streams, the exit status is the container command's (3,4,5unchanged), and the coordinator's timeout and process-group kill reach Docker. Bytes never appear in arguments. - Actor. The in-container Hermes identity is pinned to
default(HERMES_PROFILEandHERMES_PROFILE_NAME), independent of any host profile variable, so attachments are recorded as uploaded bydefault. - Native store.
hermes_helmet.native_artifactsruns inside the container and uses the runtime's own attachment functions, so the 25 MiB cap, name sanitization, collision-free storage, and attachment ids are native. It verifies the declared size and SHA-256 before storing, hashes the stored blob when listing, scopes every lookup to board and task, and writes only a fixed code to stderr. - Container prerequisites. The service is named
hermes, runs the Hermes image's interpreter at/opt/hermes/.venv/bin/python, and has this package'ssrcat/opt/hermes-helmet/src(the Hermes Helmet image already does).
Docker being absent is reported as exit 127, which the coordinator treats as an
unavailable transport.
The repository tests use an in-memory transport and an isolated fixture adapter
(tests/fixtures/artifact_adapter.py) that implements the verbs above over a
temporary directory with native-like attachment semantics. They use synthetic
ExampleCo DOCX and PPTX archives and prove the coordinator behavior: byte-identical
round trips, format metadata, revision checks, error codes, and no content or
credentials in arguments, errors, or output. They do not use a real Kanban board,
Docker, or a worker profile.
tests/test_compose_runtime.py covers helmet-compose in three layers: argument
vector, project selection, pinned actor, unsupported verbs, binary streams and
timeouts through the real executable and a stand-in docker; the exit-status
matrix over a stand-in native store; and, when a Hermes interpreter is present,
the real native Kanban store through the adapter chain. Its explicit integration
command runs a disposable Compose project on the pinned Hermes image with a
minimal idle hermes service (not the writing recipe, and no model dispatch):
HELMET_COMPOSE_INTEGRATION=1 PYTHONPATH=src \
python3 -m unittest tests.test_compose_runtime.ComposeIntegrationTests -vIt round-trips DOCX and PPTX bytes through native attachments, checks exit 3
for a missing attachment, wrong task, and wrong board, 4 for oversize input, and
5 for an integrity mismatch (calling the adapter directly, because the
coordinator would refuse those inputs first), and confirms earlier bytes are
unchanged. The unique project and its volume are removed afterwards.
Real-host acceptance is separate and belongs to the deployment adapter. It must run a real DOCX input through the real boundary, have a worker produce a revision as a durable attachment that survives scratch cleanup, and retrieve it on the coordinator with a matching checksum. A container-only path does not satisfy it. A handoff that declares an artifact the runtime cannot find must stay retryable rather than complete.
See also helmet-issue.md for the worker-runtime transport.