Halt an AI Agent While Work Is Running
This drill starts a harmless HTTP read on an Organization-owned test agent, observes its arrival at a controlled receiver, and submits a security halt before the receiver responds. It demonstrates why 202, verified worker cleanup and a settled external outcome are separate facts.
The downloadable probe runs on an external test host, outside DT. DT serves its source as a static download; no probe code is installed or executed within DT. The agent uses its existing http_request tool. The halt request contains only the normal strict JSON fields.
Prepare the test environment
Use a disposable Organization-owned agent without production integrations, schedules or event webhooks. Confirm model/worker readiness, an active license, the allowed sender, and permission to use http_request. Configure the controlled test origin through the deployment's ordinary network and security policies. Agent instructions alone do not establish those restrictions.
The deployment must meet the security halt rollout prerequisites, including process isolation and all dispatch enforcers. Use a dedicated service identity scoped to this agent with security:halt and security:read. Use a separate Organization API key with agents:operate to start and inspect ordinary agent work. The service identity does not have ordinary agent API authority.
Complete step 1 of the curl walkthrough in your command terminal. It defines DT_ORIGIN, DT_AGENT_ID, DT_WORKDIR and dt_security_request. Check that the target has no existing hold using step 2, then stop before submitting that walkthrough's idle-agent halt.
The shared setup preserves request and evidence files on shell exit and clears both shell credentials. Record DT_WORKDIR and keep this drill's exact request and snapshots through acceptance reconciliation and verified human recovery; archive them in protected persistent storage before explicit final cleanup.
Keep a second terminal on the same external test host for the probe. Both terminals should use the same working directory and operator account so the command terminal can observe the private probe markers.
1. Start the controlled HTTPS receiver
Download the active-work probe. It needs Python 3.12 or later and an existing TLS certificate/key for a hostname trusted by the DT worker's actual HTTP/proxy path. Replace the certificate paths, hostname and port for your test environment. Restrict access to the intended test clients; this small receiver is a lab fixture.
In the probe terminal:
umask 077
mkdir -m 700 probe-evidence
DT_PROBE_RUN=$(uuidgen | tr '[:upper:]' '[:lower:]')
printf 'Run ID: %s\n' "$DT_PROBE_RUN"
python3 active_work_probe.py \
--run-id "$DT_PROBE_RUN" --evidence-dir probe-evidence \
--host 0.0.0.0 --port 8443 --hold-seconds 10 \
--cert-file /path/to/test-fullchain.pem --key-file /path/to/test-key.pem
Wait for ready: true. In the command terminal, copy the printed run ID and set the reachable HTTPS origin:
DT_PROBE_RUN='replace-with-the-printed-run-uuid'
DT_PROBE_ORIGIN='https://probe.example.test:8443'
DT_PROBE_DIR="probe-evidence/$DT_PROBE_RUN"
DT_EVENT_ID="active-work:$DT_PROBE_RUN:halt-v1"
For this run, /hold/{run_id} records hold-received.json, waits ten seconds and returns fixed non-secret JSON. /after/{run_id} responds immediately and records after-received.json. Every accepted request is counted in events.jsonl; duplicates update the marker's count. Other paths return 404.
The built-in HTTP tool currently has a fixed 15-second timeout; it has no caller-configurable timeout parameter. Keep the probe delay below that timeout, and submit the halt promptly after observing arrival. Do not manually fetch the run's /hold or /after URL: that would contaminate the markers used to identify agent traffic. Use a separate fresh run ID for connectivity checks or a repeated attempt.
2. Prepare the halt before starting work
Create the immutable security request now, so no request construction is needed during the ten-second window:
jq -n --arg event "$DT_EVENT_ID" --arg agent "$DT_AGENT_ID" \
'{external_event_id: $event, target: {type: "agent", id: $agent},
action: "halt", reason: "Containment drill during an admitted HTTP read",
incident_reference: "ACTIVE-WORK-DRILL"}' > "$DT_WORKDIR/active-halt-request.json"
dt_submit_active_halt() {
DT_HTTP_STATUS=$(dt_security_request \
--request POST --header 'Content-Type: application/json' \
--data-binary "@$DT_WORKDIR/active-halt-request.json" \
--dump-header "$DT_WORKDIR/active-halt.headers" \
--output "$DT_WORKDIR/active-halt.json" --write-out '%{http_code}' \
"$DT_ORIGIN/api/v1/security/interventions") || {
printf 'Submission failed; preserve the request and inspect the retry guidance.\n' >&2
return 1
}
case "$DT_HTTP_STATUS" in
200|202) printf 'HTTP %s\n' "$DT_HTTP_STATUS" ;;
*) printf 'Unexpected HTTP %s; stop and inspect the response.\n' "$DT_HTTP_STATUS" >&2; return 1 ;;
esac
}
Read the ordinary API key without placing it in curl's arguments, then build the test message. Use a sender allowed by the test agent's email contact policy:
read -rsp 'Organization Operator API key: ' DT_API_KEY
printf '\n'
read -rp 'Allowed test sender email: ' DT_SENDER
dt_agent_request() {
printf 'X-Api-Key: %s\n' "$DT_API_KEY" |
curl --disable --silent --show-error --fail-with-body \
--proto '=https' --connect-timeout 10 --max-time 30 \
--header @- --header 'Accept: application/json' "$@"
}
DT_TEST_MESSAGE="Use http_request to make exactly one GET to $DT_PROBE_ORIGIN/hold/$DT_PROBE_RUN. Only after that request returns successfully, make one GET to $DT_PROBE_ORIGIN/after/$DT_PROBE_RUN, then report the two probe markers. Run the calls sequentially, without parallel dispatch, retries, browser tasks, scripts or other tools. Use no credentials or secret headers."
jq -n --arg sender "$DT_SENDER" --arg body "$DT_TEST_MESSAGE" \
'{channel: "email", sender: $sender, body: $body}' \
> "$DT_WORKDIR/active-message-request.json"
These are ordinary agent instructions, not a deterministic execution harness. Inspect what the agent actually does. If it requests approval, chooses another tool, dispatches both reads together, or finishes before the halt window, record an inconclusive attempt rather than claiming this drill passed. Never put the API key or service token in a probe URL or agent message.
3. Observe admission, then submit the halt
In the command terminal, send the prepared message and immediately start the marker wait. The message route has a trailing slash; the security route does not.
dt_agent_request --request POST --header 'Content-Type: application/json' \
--header "Idempotency-Key: active-message:$DT_PROBE_RUN" \
--data-binary "@$DT_WORKDIR/active-message-request.json" \
--output "$DT_WORKDIR/active-message.json" \
"$DT_ORIGIN/api/v1/agents/$DT_AGENT_ID/messages/" \
&& DT_START_CURSOR=$(jq -er '.event.cursor' "$DT_WORKDIR/active-message.json") && {
for DT_ATTEMPT in $(seq 1 60); do
if [ -s "$DT_PROBE_DIR/hold-received.json" ]; then break; fi
sleep 1
done
if [ ! -s "$DT_PROBE_DIR/hold-received.json" ]; then
printf 'No admitted probe observed; inspect the agent and record an inconclusive attempt.\n' >&2
elif [ -e "$DT_PROBE_DIR/hold-response-attempted.json" ] \
|| [ -e "$DT_PROBE_DIR/after-received.json" ]; then
printf 'The intended in-flight window was missed; preserve evidence and review before retrying.\n' >&2
else
dt_submit_active_halt
fi
}
The message's 201 is queue acceptance. processing_active: true, a Working indicator or a timeline step alone does not prove external dispatch. The fresh receiver marker proves this read reached the external host before you sent the halt. Keep its run ID/count and the returned message cursor.
After a successful security 202 or identical replay 200, save the intervention ID and inspect the initial evidence:
DT_INTERVENTION_ID=$(jq -er '.id' "$DT_WORKDIR/active-halt.json")
DT_STATUS_PATH=$(jq -er '.status_url' "$DT_WORKDIR/active-halt.json")
DT_CONTROL_PATH=$(jq -er '.control_state_url' "$DT_WORKDIR/active-halt.json")
jq '{id, state, hold_active, enforcement, accepted_at}' "$DT_WORKDIR/active-halt.json"
cat "$DT_PROBE_DIR/hold-received.json"
Run these commands only after success; a missing, partial or older response file does not establish acceptance. If the POST response is lost, repeat this drill's submission helper with its saved active-halt-request.json, then extract the identifiers after success:
dt_submit_active_halt
The identical retry rules apply: keep this same identity, event and body. Do not use a new event ID. Preserve an accepted hold even if you later discover the intended timing was missed, and complete human recovery before starting another attempt.
4. Read cancellation and worker evidence independently
Poll the direct security reads with a bounded wait. These reads do not depend on the held agent's queue:
for DT_DELAY in 1 2 4 8 8 8 8 8 8 8 8 8; do
dt_security_request --output "$DT_WORKDIR/active-status.json" \
"$DT_ORIGIN$DT_STATUS_PATH" || break
dt_security_request --output "$DT_WORKDIR/active-control.json" \
"$DT_ORIGIN$DT_CONTROL_PATH" || break
jq '{id, state, hold_active, enforcement}' "$DT_WORKDIR/active-status.json"
jq '{held, effective_state, active_intervention_ids, barriers, workers, in_flight}' \
"$DT_WORKDIR/active-control.json"
if jq -e '.state == "failed" or .state == "released"' \
"$DT_WORKDIR/active-status.json" > /dev/null; then break; fi
if jq -e '.workers.status == "verified" and .workers.acknowledged == true' \
"$DT_WORKDIR/active-control.json" > /dev/null; then break; fi
sleep "$DT_DELAY"
done
Missing acknowledgement or an expired polling budget requires operator follow-up; it does not imply process absence or remove the hold. Stop and inspect authentication/scope/transport errors. The two responses are separate snapshots, so account for another intervention or human release racing with the reads.
For one tracked runtime and one admitted HTTP call, an initial projection can look like this; counts vary with the actual workload:
{
"state": "accepted",
"hold_active": true,
"enforcement": {
"admission_barrier": "active",
"dispatch_barrier": "active",
"worker_acknowledgement": "pending",
"known_work": {"pending": 0, "cancellation_requested": 1, "cancelled": 0, "terminal": 0, "unknown": 1, "failed": 0}
}
}
After runtime termination is verified, a call that is not read-only (for example a POST) or whose runtime was not yet proven absent stays unresolved, so a later projection can be:
{
"state": "partially_enforced",
"hold_active": true,
"enforcement": {
"admission_barrier": "active",
"dispatch_barrier": "active",
"worker_acknowledgement": "acknowledged",
"known_work": {"pending": 0, "cancellation_requested": 0, "cancelled": 1, "terminal": 0, "unknown": 1, "failed": 0}
}
}
The cancelled item here is the terminated DT runtime; it is not proof that the provider cancelled the HTTP call. Effective target counts use known in place of pending and deduplicate shared work.
| Observation | Meaning |
|---|---|
202, accepted, active barriers | The durable hold committed. Runtime termination and external outcomes still need evidence. |
enforcing, cancellation requested, worker pending | Cancellation/verification is in progress. Old heartbeat age or an idle-looking UI does not prove absence. |
Worker verified / acknowledged, partially_enforced, unknown work | DT-owned runtime cleanup is verified; the already admitted call remains unresolved. Keep the hold and investigate. |
enforced with all known work terminal/cancelled | Included cleanup and tracked outcomes settled. The hold is still active; completed external effects were not undone. |
failed | Required cleanup or verification failed. Admission remains blocked; an authorized operator may retry enforcement. |
An enforced projection can instead have worker_acknowledgement: acknowledged, cancellation_requested: 0, cancelled: 1, terminal: 1 and unknown: 0 when the runtime terminated and DT durably observed the HTTP outcome. A plain read may settle quickly; this drill does not require every run to remain unknown. The drill's GET is declared read-only, so once the runtime process is verified absent its tool call is reported cancelled rather than unknown; unknown that remains means the absence was not proven yet or the call was not read-only. A model call that was in flight when the hold landed stays unknown, because the provider may keep generating. Neither state proves the receiver stopped.
5. Check the receiver and blocked continuation
After the probe's delay, inspect events.jsonl. hold-response-written means the receiver's local write finished, while hold-client-disconnected means its write saw a disconnect. Neither proves that DT received or persisted a terminal result. A provider's log is useful investigation evidence; it must not be used to manually clear DT's Unknown state or worker verification.
There should be no new /after dispatch from the old execution after the halt wins admission. Inspect the timeline from the saved message cursor and, with the responsible operator, the generation-bound dispatch receipts and security lifecycle evidence. Ordinary processing status is useful context, but the security reads supply containment evidence:
dt_agent_request --output "$DT_WORKDIR/active-processing.json" \
"$DT_ORIGIN/api/v1/agents/$DT_AGENT_ID/processing-status/"
dt_agent_request --get --data-urlencode "cursor=$DT_START_CURSOR" \
--data-urlencode 'direction=newer' --data-urlencode 'limit=100' \
--output "$DT_WORKDIR/active-timeline.json" \
"$DT_ORIGIN/api/v1/agents/$DT_AGENT_ID/timeline/"
jq '{processing_active, processing_snapshot}' "$DT_WORKDIR/active-processing.json"
jq '.events' "$DT_WORKDIR/active-timeline.json"
Receiver timestamps alone cannot order DT's transaction boundary: a later arrival may have been admitted before the halt. If after-received.json exists, resolve its committed admission/generation and do not label pre-admitted work as a fence failure. If it was genuinely dispatched from the old execution after the halt committed, retain the evidence and report a defect. If there is no second call, verify the agent had actually planned the sequential continuation; absence alone does not prove the fence was exercised.
Use the direct reads again if late evidence arrives. A trusted terminal observation may move work from Unknown to terminal without continuing the agent. No ambiguous external action should be automatically repeated.
6. Recover deliberately and retain evidence
Record the intervention ID, run ID, message cursor, exact deployed revisions, acceptance time, runtime generation/termination evidence, worker acknowledgement, both security snapshots, receiver counts and any unresolved outcome. This drill covers one controlled read; a full validation also includes other dispatchers, worker restart, stale deliveries and telemetry outage. Source tests or writing this guide do not mark a live qualification pass.
After human incident review, follow the release and final verification flow. Confirm the exact agent name, acknowledge any unresolved outcomes, and resolve failed cleanup before final release. Read both the released intervention and effective target state; Unknown can remain after release and other holds may still apply.
Release must not automatically execute the saved /after continuation or replay the cancelled turn. Only a deliberate fresh human request through normal admission checks may start new work. Use a new probe run for that separate test so old and new traffic cannot be confused.
Stop the external probe with Ctrl+C, preserve its evidence with the incident record, retire only this drill's credential and test agent after verified recovery, and unset DT_API_KEY and the service token. If you execute this against a deployed DT application, record your actual observations and cleanup in your own change record; leave unobserved variants unverified.