Host an external execution agent
Use this when a third-party host should run slices instead of thespider-agent. The host stays thin: execute the prompt, return stdout. Parsing and findings stay on the server.
You need a project-scoped token, the project id, profile, enabled model codes, a checkout at the campaign commit, and a runner that follows the runner contract.
Claim
POST /v1/campaigns/42/claim
Authorization: Bearer TOKEN
Content-Type: application/json
{
"client_id": "runner-east-1",
"models": ["anthropic/claude-sonnet-5"],
"max_slices": 2,
"lease_seconds": 3600
}
Empty leases[] means nothing claimable. Keep lease_id, slice_id, model_code, prompt, prompt_hash, marker, timeout_seconds, and lease_expires_at until you submit. Write the exact prompt bytes to the runner. Do not rebuild it.
Run
- SHA-256 of the prompt bytes must equal
prompt_hash. - Run the model in the campaign's checkout.
- Honor
timeout_seconds(or a longer lease you requested). - Capture stdout, exit code, duration, and a short stderr tail.
- Do not parse findings on the host.
While it runs, heartbeat:
POST /v1/leases/9001/heartbeat
Authorization: Bearer TOKEN
A missed heartbeat is not a reason to kill the model. If the lease expires, a late result can still win if nobody else finished the slice.
Submit
POST /v1/leases/9001/results
Authorization: Bearer TOKEN
Content-Type: application/json
{
"exit_code": 0,
"duration_ms": 182345,
"model_code": "anthropic/claude-sonnet-5",
"client_version": "external-host/1.0.0",
"prompt_hash": "HASH_FROM_CLAIM",
"stdout": "RAW_PROCESS_STDOUT",
"stderr_tail": "LAST_4000_CHARACTERS"
}
exit_code is required. It decides whether the run counts as coverage, so it must be a JSON
number — the process's real exit status — or null, which means "the runner was killed before it
exited" (a timeout) and is treated as a failed run. Omitting it, or sending it as a string like
"0", is a payload error and answers 400, not 422: a 422 would tell you the model failed
when the problem is the request body.
| Status | Meaning | What you do |
|---|---|---|
200 | This lease won with valid coverage | Record accepted |
202 | Someone else won; yours is stored as a leftover | Stop retrying |
400 | missing_exit_code / invalid_exit_code — the field is absent or not a number/null | Fix the payload and resubmit; retrying unchanged always fails |
409 | prompt_hash mismatch | Discard, claim again |
413 | Artifact too large | Shrink output, claim and rerun |
422 | Marker/JSON invalid, or nonzero exit | Claim again; do not count the slice as done |
429 / 5xx | Rate limit or server blip | Back off and retry |
Retries of the same (lease_id, prompt_hash) do not ingest twice.
If the host crashes: persist the lease and the result body. On restart, submit an unsent result even if the lease has expired. If there is no result, do not invent one. Let the lease expire.
Do not upload the source tree. Treat prompt and model output as tenant data. Token stays out of prompts and logs.