Chapter 30
Chapter 30 — Job Artifacts
Since v0.88.0.
Dispatch work to a thClaws worker over HTTP, then collect the files
it produced — declared per job, frozen with a hash the moment the run
finishes, downloadable with the same Bearer token you dispatched with.
Push input files the same way. Together these turn any --serve
instance into a worker node you can chain into pipelines:
coder (machine A) ──artifacts──▶ your orchestrator ──inputs──▶ reviewer (machine B)
If you’ve used GitHub Actions or GitLab CI artifacts, this is the same
idea — declared outputs of one job, retrievable by id — applied to
agent runs. The “job” is the session_id every /agent/run returns.
Why not workspace sync?
The sync surface — the /workspace/sync/* routes that
Chapter 27’s /cloud push and /cloud pull
ride on — mirrors a whole workspace and was designed for trusted
networks (a tunnel or ForwardAuth in front). An external orchestrator holding only an API
token had three gaps: no supported auth, no way to know which files a
given job actually produced, and a race — the file list and the file
download were separate requests, so a file could change in between.
Job Artifacts closes all three: Bearer auth, per-job scoping, and a
manifest whose hashes are fixed at collection time.
Why not A2A (or ACP)?
The other obvious question: there are already open protocols for agents to talk to each other. Why a thClaws-specific endpoint?
A2A (Agent2Agent Protocol) is Google’s open protocol, now under the Linux Foundation, for driving an agent built by someone else: capabilities advertised in an Agent Card, work dispatched as tasks over JSON-RPC, progress streamed with SSE. ACP is two different things that share a name — Agent Communication Protocol (IBM/BeeAI), which has since merged into A2A, and Agent Client Protocol (Zed), an editor ↔ coding-agent protocol for embedding an agent in an editor pane. The second is a different problem entirely from shipping work between machines.
The reason Job Artifacts still exists: A2A is a conversation layer; artifacts are a storage layer. An A2A artifact is message parts flowing back with a task while you talk — it carries no durability promise. A thClaws artifact is a real file from the workspace, frozen into a snapshot and hashed the moment the run ends, and re-fetchable later by id even after the source file has changed (the CI-artifact semantics from the comparison above).
In practice an orchestrator holding nothing but an API token needs
three curl calls, not an A2A client. And if thClaws grows an A2A
facade later, it will serve its results out of this same artifact
store — the two are complements, not alternatives.
Quick start
Start a worker:
THCLAWS_API_TOKEN=secret thclaws --serve --port 8443
1. (Optional) push input files before dispatching:
curl -X POST http://worker:8443/v1/inputs \
-H "Authorization: Bearer secret" -H "Content-Type: application/json" \
-d '{"files":[{"path":"inputs/brief.txt","content_base64":"'"$(base64 < brief.txt)"'"}]}'
Files land under inputs/ in the worker’s workspace (see
Input placement rules).
2. Dispatch with collect_files — glob patterns naming what counts
as this job’s output:
curl -X POST http://worker:8443/agent/run \
-H "Authorization: Bearer secret" -H "Content-Type: application/json" \
-d '{"prompt":"Read inputs/brief.txt and write a report to reports/summary.md",
"collect_files":["reports/*.md"]}'
# → { "session_id": "sess-abc…", "summary": "...", ... }
When the run finishes, matching files are copied into a per-session snapshot and sha256-hashed. From that moment the artifact is immutable — editing the live file later changes nothing you can download.
3. Fetch the manifest, then the files:
curl -H "Authorization: Bearer secret" \
http://worker:8443/v1/sessions/sess-abc…/artifacts
# → { "artifacts": [ { "id":"a1", "path":"reports/summary.md",
# "size":180, "sha256":"67aeef…" } ],
# "patterns": ["reports/*.md"], "collected_at": "…" }
curl -H "Authorization: Bearer secret" -o summary.md \
http://worker:8443/v1/sessions/sess-abc…/artifacts/a1
# response carries an `x-sha256` header — verify it matches the manifest
Endpoint reference
All under the same Bearer auth as the rest of /v1
(THCLAWS_API_TOKEN).
| Endpoint | What it does |
|---|---|
POST /agent/run + "collect_files": ["glob", …] |
Declare the run’s outputs; snapshot + hash at completion. Works on the sync, streaming (SSE), and async (x_callback) paths alike. |
GET /v1/sessions/{sid}/artifacts |
The frozen manifest: id, path, size, sha256 per artifact, plus the patterns used and a skipped list if caps truncated the collection. |
GET /v1/sessions/{sid}/artifacts/{aid} |
One file, served from the snapshot (never the live workspace). aid is the manifest id (a1, a2, …) or the exact path. |
POST /v1/inputs |
Place files into the workspace ahead of a dispatch. Body: {"workspace_dir"?: "...", "files": [{"path", "content_base64"}]}. |
Both GETs and /v1/inputs accept an optional workspace_dir
(query parameter for the GETs) with the same validation as
/agent/run; omitted, the daemon’s working directory is used.
Input placement rules
POST /v1/inputs is deliberately conservative:
- Paths must be relative, may not contain
.., and may never touch.thclaws/or.git/. - By default files may only land under
inputs/. Widen with an env allowlist on the worker:THCLAWS_INPUTS_PREFIXES="inputs/,data/"— orTHCLAWS_INPUTS_PREFIXES="*"for anywhere in the workspace (still excluding.thclaws/and.git/). - Limits: ≤ 100 files per request, ≤ 64 MB decoded total. The response
echoes each written file’s
sha256so the sender can verify.
An empty manifest is not a failure
Two outcomes look similar from the outside and mean very different things, so the manifest states which one you got:
status |
Meaning |
|---|---|
completed |
The snapshot ran. artifacts may still be empty — a pattern that matched nothing is a completed snapshot of nothing |
failed |
The snapshot was requested but not written. An error field says why |
Collection failing does not fail the run. That is deliberate: the
agent did its work, and losing the file copy afterwards shouldn’t
retroactively turn a good run into a bad one. So an orchestrator has to
read status — treating “no artifacts” as “the job broke” will make
you retry runs that succeeded.
The error is carried in the manifest rather than only logged, so a
caller learns the outcome from the API instead of having to read the
worker’s stderr.
Collection limits
Artifact snapshots cap at 256 files / 300 MB per run. Files that
matched your globs but fell past a cap appear in the manifest’s
skipped list — a truncated collection is always visible, never
silent. Collection skips .thclaws/, .git/, and node_modules/
entirely.
Securing workspace sync for orchestrators (Tier 1)
If you do want whole-workspace mirroring (Chapter 27) from an orchestrator that only holds the API token, opt the sync routes into Bearer auth on the worker:
THCLAWS_SYNC_REQUIRE_AUTH=1 THCLAWS_API_TOKEN=secret thclaws --serve
Every /workspace/sync/* request must then carry
Authorization: Bearer <token> — no tunnel or ForwardAuth needed.
Unset, sync behaves exactly as before (trusted-network model), so
existing deployments are unaffected.
A two-worker pipeline, end to end
# machine A writes code
SID_A=$(curl -s -X POST http://worker-a:8443/agent/run \
-H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
-d '{"prompt":"Implement the parser per spec.md","collect_files":["src/**/*.py"]}' \
| jq -r .session_id)
# pull A's artifacts, push them to B as inputs
curl -s -H "Authorization: Bearer $TOK" \
http://worker-a:8443/v1/sessions/$SID_A/artifacts | jq -c '.artifacts[]' |
while read -r art; do
aid=$(jq -r .id <<<"$art"); path=$(jq -r .path <<<"$art")
curl -s -H "Authorization: Bearer $TOK" \
http://worker-a:8443/v1/sessions/$SID_A/artifacts/$aid -o /tmp/f
curl -s -X POST http://worker-b:8443/v1/inputs \
-H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
-d "{\"files\":[{\"path\":\"inputs/$path\",\"content_base64\":\"$(base64 < /tmp/f)\"}]}"
done
# machine B reviews the actual files
curl -s -X POST http://worker-b:8443/agent/run \
-H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
-d '{"prompt":"Review the code under inputs/ and write findings to review.md",
"collect_files":["review.md"]}'
Where artifacts live on disk
Snapshots are stored inside the worker’s workspace at
.thclaws/state/artifacts/<session_id>/ (manifest.json + files/).
Like everything under .thclaws/state/, they’re gitignored and never
included when an agent is packed or published. They persist until you
delete them — prune old sessions’ directories if a long-lived worker
accumulates too many.
See also
- Chapter 3 —
--servemode andTHCLAWS_API_TOKEN. - Chapter 19 — heartbeat schedules; combine with artifacts for continuous produce-and-collect loops.
- Chapter 27 — workspace sync, the whole-workspace alternative.
- Technical manual:
job-artifacts.mdfor wire-level details.