Skip to main content

Security Response API: Halt Agents From Your SIEM

This vendor-neutral Core MVP is deployment-gated and supports Organization-owned agents only. It does not support run-specific targets, tenant-wide containment, steering, instructions, expiry or external release. Vendor-specific adapter packages are separate work.

Start with Configure external security for the administrator walkthrough and Enable response for installation readiness. Monitoring-only integrations use the independent audit forwarding path. This page documents the optional response contract and retains its existing URL.

For a complete integration exercise, follow the curl walkthrough. For a runnable incident adapter, use the Python handler with trusted asset mapping, durable retries and bounded polling. For an active execution, follow the halt while work is running drill using a controlled external HTTP receiver.

Authentication and operations​

An Owner, Admin or Security Admin creates one service identity per integration in Governance → Policies → External security → Response access. Each identity has an explicit agent allowlist. The core permission model supports security:halt and/or security:read; the UI offers these individually and allows name, permissions and explicit agent-scope edits. Auditor is read-only. Existing tokens use current scope/permissions; edits serialize with halt admission. Disabling Organization or installation response rejects new external authentication/admission while preserving interventions and human release/cleanup retry. Save the one-time token securely; subsequent reads never return it. Rotation atomically revokes the old credential, and disable/revoke takes effect on the next request without clearing existing holds.

Use Authorization: Bearer <token> over HTTPS, never a query string or ordinary X-Api-Key. Identity and Organization come from authentication, not JSON fields.

An integration that creates holds and polls them needs both permissions. A halt-only identity can create and replay its requests but cannot use either GET route. Read access is scoped to the agent, not the requesting identity: it can retrieve a known intervention ID for that agent, including internal holds and holds created by other identities in the same Organization, with its reason and incident reference. Grant read access accordingly. The public API has no intervention-list, release, enforcement-retry or agent-resume operation; keep accepted IDs in your incident record and use DT's human recovery controls.

  • Create a halt: POST /api/v1/security/interventions, requiring security:halt.
  • Read an intervention: GET /api/v1/security/interventions/{id}, requiring security:read and current target scope.
  • Read effective target state: GET /api/v1/security/targets/agent/{id}/control-state, requiring security:read and current target scope.
{
"external_event_id": "incident-4812:halt-v1",
"target": {"type": "agent", "id": "8fe80423-34d1-4f17-95ad-aac7597cd201"},
"action": "halt",
"reason": "Credential theft alert linked to this agent",
"incident_reference": "INC-004812"
}

Send UTF-8 JSON, at most 16 KiB. Only the fields shown above are supported. Unknown fields and unsupported actions/target types return 400; an oversized body returns 413. Event ID, reason and optional reference are NFC-normalized and trimmed before validation and deduplication. Event ID must contain 1–160 characters, reason 1–1,000, and reference at most 255. An omitted, null or blank reference becomes an empty string in responses. An invalid target UUID returns 400; a valid UUID for a wrong-Organization, missing or out-of-scope target returns 404. The adapter must correlate the DT agent UUID through a controlled asset mapping; alerts do not establish target ownership.

Connect your sender​

Map a controlled incident/action identifier to external_event_id and preserve it across retries. Do not generate a new ID for each delivery attempt. Identity and Organization come from authentication; do not supply tenant fields. Keep the reason/reference fixed on retries, and use a new event ID only for a deliberate new containment request.

Store the example JSON as halt.json in a private working directory. For a command-line request, put the bearer header in a private curl configuration file through your secret store, with permissions limited to the integration operator. Its header entry has the form header = "Authorization: Bearer <token>". Avoid passing the token in shell arguments or printing that file:

curl --config /private/security-response/curl.conf --header 'Content-Type: application/json' --data-binary @halt.json https://dt.example/api/v1/security/interventions
curl --config /private/security-response/curl.conf 'https://dt.example/api/v1/security/interventions/<intervention-uuid>'
curl --config /private/security-response/curl.conf 'https://dt.example/api/v1/security/targets/agent/<agent-uuid>/control-state'

Replace the URL and UUID placeholders before executing. The public routes have no trailing slash. Retain the returned intervention ID, status_url and control_state_url; poll with the same currently authorized identity. Respect the sender's retry/backoff budget, retry ambiguous creation with unchanged JSON/event ID, and inspect a conflict rather than sending a new ID automatically.

Direct webhook use requires a sender that can set the exact bearer header and JSON body. If it cannot, use a narrowly scoped trusted relay with controlled asset mapping and credential storage. DT does not accept arbitrary vendor alert payloads directly or provide shipped Splunk/Sentinel/Wazuh response adapters.

ResponseMeaning and next step
202 on createDurable acceptance; retain IDs and inspect status
200, replayed: true on createSame previously accepted event, with current status; no second hold
400Repair strict JSON, field bounds, target type or supported action
401Repair bearer credential, enabled/revoked state, HTTPS or deployment readiness
403Identity lacks the required security permission
404Target/intervention unavailable or outside current scope; do not infer its existence
409 external_event_id_conflictEvent ID reused with different semantics; review the original request
409 agent_emergency_unavailableProcess isolation is disabled; have the operator complete runtime setup
409 agent_emergency_owner_changedOwnership changed during halt admission; review the target with the operator
413 request_too_largeReduce the UTF-8 JSON body to at most 16 KiB
500 or a lost responseAcceptance may have committed; retain error_id if returned and retry the unchanged event/body with bounded backoff

Acceptance is not termination proof​

202 means the intervention, emergency barrier and local evidence committed. Retain the intervention ID and status_url; poll directly, independently of the agent queue. An identical identity/event retry returns 200 with the original/current intervention. Different semantics return 409 external_event_id_conflict. A replay after release does not halt again; a new request needs a new event ID. Deduplication records persist for the Organization lifetime.

Understand status​

hold_active is independent of lifecycle. Inspect admission/dispatch barriers, worker acknowledgement, and categorized work counts separately:

StateEvidence
acceptedHold, barriers and local evidence committed; cleanup may not have started
enforcingBarriers remain active; cancellation and reconciliation are in progress
enforcedRequired cleanup verified and all known work for this intervention terminal or cancelled
partially_enforcedIncluded cleanup verified, but tracked work still pending, cancellation requested, unknown or failed
failedRequired DT cleanup/verification failed; the hold remains active
releasedThis hold inactive; another hold may still apply

Work counts distinguish pending, cancellation requested, cancelled, terminal, unknown, and failed outcomes. Target status deduplicates work shared by overlapping holds. The current implementation returns the same intervention-row timestamp for state_updated_at and updated_at; neither is a timestamp for every change in work evidence. Use the returned lifecycle and work counts when polling. Unknown outcomes can remain visible after release until trusted evidence settles them. Missing worker acknowledgement is not process-absence proof, and an admitted external call can complete remotely despite local connection closure.

cancelled is a proven stop, or work shown never to have started: a DT-owned execution unit whose absence was verified (the agent runtime, or a browser task whose process tree is shown to be gone), a read-only tool call whose own runtime process was verified absent, a request that never left before the hold, or a remote task whose server confirmed cancelled on a later status read. A request the receiving service may have accepted (an approved API call, an email, a webhook, a model call, any write) stays unknown even when DT closed its own connection, and an unknown outcome is never retried automatically. The status API exposes no termination class or per-surface field; the operator console does. Whether work outside the agent runtime is terminated at all depends on the deployment (a dedicated browser worker, the shared-service attribution key and control sockets), it is not guaranteed, and a closed connection or a recorded time never proves the other side stopped.

worker_acknowledgement and workers.acknowledged describe shared DT runtime cleanup only. An acknowledgement can precede completion of other required cleanup and does not certify provider cancellation. A nonzero work failed count is unresolved evidence and can yield partially_enforced; lifecycle failed instead indicates missing or failed required emergency-control verification. For effective target state, active failed holds take precedence over partially_enforced, then all-enforced, enforcing and accepted. A legacy emergency latch can also keep held: true without active intervention IDs.

Status GETs reconcile durable work and lifecycle evidence before returning; they can record transitions without executing the held agent. Runtime teardown and remote cancellation still need healthy enforcing workers and reconcilers. These endpoints have no long-poll or completion callback contract, and there is no enforcement-time SLA. Bound polling and hand unresolved evidence to an authorized human.

The endpoint reference marks every emitted success field as required, including transition timestamps that may be null. POST returns replayed: false for 202 and true for an identical retry's 200; GET reads return false. Effective agent state uses active when no hold remains, while unresolved external outcomes may still be present. Per-intervention work counts use pending; effective target counts use known and count shared work once. The in_flight object is a categorized evidence inventory, not a count of currently executing processes: it includes terminal/cancelled work for active holds and unresolved work from released holds. Settled work from released holds drops out of target counts but remains in that intervention's evidence.

Successful responses include Cache-Control: no-store. Both POST success statuses also include Location, equal to the relative status_url; resolve it against the same deployment origin. Authentication failures use a detail body and WWW-Authenticate: Bearer realm="security-interventions". A POST whose credential becomes unavailable before the locked acceptance check instead returns 401 with error/message and no challenge. Create target failures use 404 target_not_found with error/message; ordinary read lookup failures use detail, while a control-state ownership race can use target_not_found. Unexpected server errors include detail and a support error_id. Exact response schemas and headers are in each endpoint reference above.

The dispatch boundary commits generation-bound admission evidence before external I/O. Halt-first prevents invocation; dispatch-first is reported as admitted work. Read tools, deferred browser/MCP/OpenAPI/email and webhook/callback dispatchers are included. No ambiguous external effect is automatically repeated. Optional agent auditing does not disable mandatory dispatch receipts.

End-to-end curl walkthrough​

This exercise uses an idle Organization-owned test agent with no existing holds. It places a real hold, reads its enforcement evidence, releases it through DT as a human, and checks the final state. The example UUIDs, incident reference and timestamps below are illustrative; use your deployment's values. This walkthrough does not replace validating your deployment's worker, restart and dispatch behavior.

1. Create a scoped identity and prepare your shell​

Have the installation operator confirm that the deployment prerequisites in Recovery and rollout are complete and your Organization is enabled. As an Owner, Admin or Security Admin, open Governance → Policies → External security → Response access:

  1. Enter an Integration identity name, such as curl-walkthrough.
  2. Select only your test agent under Authorized agents.
  3. Select Request agent halts (security:halt) and Read agent and intervention status (security:read), then choose Create identity and token. Read-only access is the default; confirm both permissions in the created identity.
  4. Save the One-time bearer token securely, then choose I saved it. Record the agent's UUID from your controlled asset mapping and its exact name from the agent details. Alerts alone do not establish target ownership.

Run the following in one dedicated Bash session with curl and jq installed. Your curl must support --fail-with-body and reading headers from a file. Set the HTTPS origin without a trailing slash or path, and replace the agent UUID and event/reference values. Choose a new event ID for this exercise, then keep it unchanged for retries.

set +x
umask 077
DT_ORIGIN='https://dt.example.com'
DT_AGENT_ID='8fe80423-34d1-4f17-95ad-aac7597cd201'
DT_EVENT_ID='walkthrough:INC-004812:halt-v1'
DT_INCIDENT_REFERENCE='INC-004812'
DT_WORKDIR=$(mktemp -d)
trap 'unset DT_TOKEN DT_API_KEY' EXIT
printf 'Saved request and evidence directory: %s\n' "$DT_WORKDIR"
read -rsp 'Security service bearer token: ' DT_TOKEN
printf '\n'

dt_security_request() {
printf 'Authorization: Bearer %s\n' "$DT_TOKEN" |
curl --disable --silent --show-error --fail-with-body \
--proto '=https' --connect-timeout 10 --max-time 30 \
--header @- --header 'Accept: application/json' "$@"
}

The helper sends the token through curl's standard input, keeping it out of command history and curl's argument list. Keep shell tracing and curl verbose/trace output disabled. Record the private directory's path, deployment origin, service identity and event ID before submitting; add the intervention ID after acceptance.

Shell exit clears credentials and preserves the saved request and evidence, including after an ambiguous POST. Retain these files until acceptance is reconciled and human recovery is verified. Temporary storage can be cleared by the host; archive unresolved incident files in protected persistent storage. In a replacement shell, restore DT_WORKDIR to the retained directory and configure the same origin, identity and request helper with its current credential. Retry the saved request without rebuilding its body or choosing another event ID.

2. Inspect the target before halting it​

dt_security_request \
--output "$DT_WORKDIR/before.json" \
"$DT_ORIGIN/api/v1/security/targets/agent/$DT_AGENT_ID/control-state"
jq . "$DT_WORKDIR/before.json"
jq -e '.held == false and .effective_state == "active"' \
"$DT_WORKDIR/before.json"

Stop if the request or assertion fails. A successful 200 for an agent with no previous control or tracked work can look like this:

{
"target": {"type": "agent", "id": "8fe80423-34d1-4f17-95ad-aac7597cd201"},
"held": false,
"effective_state": "active",
"active_intervention_ids": [],
"generation": 0,
"barriers": {"admission": "inactive", "dispatch": "inactive"},
"workers": {"acknowledged": false, "status": "not_applicable"},
"in_flight": {
"known": 0,
"cancellation_requested": 0,
"cancelled": 0,
"terminal": 0,
"unknown": 0,
"failed": 0
},
"as_of": "2026-10-02T14:00:00+00:00"
}

active means the security control is open; it does not show that an agent is executing. If held is already true, review the existing holds with an authorized human before continuing this exercise.

3. Submit the halt and save its identifiers​

Build the request once so every retry uses the same event ID, target, reason and incident reference:

jq -n \
--arg event "$DT_EVENT_ID" \
--arg agent "$DT_AGENT_ID" \
--arg incident "$DT_INCIDENT_REFERENCE" \
'{external_event_id: $event, target: {type: "agent", id: $agent},
action: "halt", reason: "Containment walkthrough for an idle test agent",
incident_reference: $incident}' > "$DT_WORKDIR/halt-request.json"

dt_submit_halt() {
DT_HTTP_STATUS=$(dt_security_request \
--request POST --header 'Content-Type: application/json' \
--data-binary "@$DT_WORKDIR/halt-request.json" \
--dump-header "$DT_WORKDIR/halt.headers" \
--output "$DT_WORKDIR/halt.json" --write-out '%{http_code}' \
"$DT_ORIGIN/api/v1/security/interventions") || {
printf 'Request failed; inspect halt.json if available and follow the retry guidance below.\n' >&2
return 1
}
case "$DT_HTTP_STATUS" in
200|202) printf 'HTTP %s\n' "$DT_HTTP_STATUS"; jq . "$DT_WORKDIR/halt.json" ;;
*) printf 'Unexpected HTTP %s; stop and inspect the response.\n' "$DT_HTTP_STATUS" >&2; return 1 ;;
esac
}

dt_submit_halt

A new accepted request returns 202, Cache-Control: no-store, and Location: /api/v1/security/interventions/7eeb0143-4498-4bd6-bdac-a5fe67e0d198. Its complete body can look like this:

{
"id": "7eeb0143-4498-4bd6-bdac-a5fe67e0d198",
"action": "halt",
"target": {"type": "agent", "id": "8fe80423-34d1-4f17-95ad-aac7597cd201"},
"state": "accepted",
"hold_active": true,
"external_event_id": "walkthrough:INC-004812:halt-v1",
"reason": "Containment walkthrough for an idle test agent",
"incident_reference": "INC-004812",
"accepted_at": "2026-10-02T14:00:10+00:00",
"enforcing_at": null,
"enforced_at": null,
"failed_at": null,
"state_updated_at": "2026-10-02T14:00:10+00:00",
"updated_at": "2026-10-02T14:00:10+00:00",
"released_at": null,
"status_url": "/api/v1/security/interventions/7eeb0143-4498-4bd6-bdac-a5fe67e0d198",
"control_state_url": "/api/v1/security/targets/agent/8fe80423-34d1-4f17-95ad-aac7597cd201/control-state",
"enforcement": {
"admission_barrier": "active",
"dispatch_barrier": "active",
"worker_acknowledgement": "pending",
"known_work": {
"pending": 0,
"cancellation_requested": 0,
"cancelled": 0,
"terminal": 0,
"unknown": 0,
"failed": 0
}
},
"replayed": false
}

Only after a successful 200 or 202, extract the identifiers and resolve the returned paths against the same origin:

DT_INTERVENTION_ID=$(jq -er '.id' "$DT_WORKDIR/halt.json")
DT_STATUS_PATH=$(jq -er '.status_url' "$DT_WORKDIR/halt.json")
DT_CONTROL_PATH=$(jq -er '.control_state_url' "$DT_WORKDIR/halt.json")
printf 'Event: %s\nIntervention: %s\n' "$DT_EVENT_ID" "$DT_INTERVENTION_ID"

Persist those two identifiers in your incident record. 202 proves durable acceptance and active barriers; it does not prove worker cleanup or termination of admitted external work.

4. Retry an ambiguous delivery without creating another hold​

If the POST times out or loses its response, it may already have committed. Re-run dt_submit_halt with the saved request and the same identity, then extract the identifiers after success. A failed attempt may leave a missing, incomplete or older response file; use it to extract identifiers only after a successful submission. You can also run it once after a successful submission to observe deduplication:

dt_submit_halt
jq '{id, state, hold_active, replayed}' "$DT_WORKDIR/halt.json"

An identical retry returns 200 with the same ID and replayed: true. The state may already have advanced. Changing the event ID after a timeout creates a separate hold; changing the body while retaining the event ID returns 409 external_event_id_conflict. After human release, replaying this same request returns the released intervention and does not halt again.

For HTTP errors, inspect halt.json and the error contracts: 401 requires correcting the credential or deployment gate, 403 requires the needed identity permission, and 404 requires checking the UUID, Organization and explicit allowlist. Fix 400/413 request validation failures. A 409 may also report an unavailable runtime or changed ownership; inspect its error code. Resolve a deduplication conflict from the original incident record. Do not create a replacement event ID to work around an uncertain result or conflict.

5. Poll enforcement with a bounded wait​

This loop reads status at most 12 times, with increasing delays capped at eight seconds and a 30-second limit per request. Its budget is an example, not an enforcement SLA. Each saved response is complete; jq displays only the evidence needed here.

for DT_DELAY in 1 2 4 8 8 8 8 8 8 8 8 8; do
if ! dt_security_request --output "$DT_WORKDIR/status.json" \
"$DT_ORIGIN$DT_STATUS_PATH"; then
printf 'Status read failed; stop polling and inspect status.json.\n' >&2
break
fi
jq '{id, state, hold_active, enforcement}' "$DT_WORKDIR/status.json"
DT_STATE=$(jq -er '.state' "$DT_WORKDIR/status.json")
case "$DT_STATE" in
enforced|partially_enforced|failed|released) break ;;
accepted|enforcing) sleep "$DT_DELAY" ;;
*) printf 'Unexpected state; stop and review the response.\n' >&2; break ;;
esac
done

For an idle agent whose cleanup completes, the displayed projection can become:

{
"id": "7eeb0143-4498-4bd6-bdac-a5fe67e0d198",
"state": "enforced",
"hold_active": true,
"enforcement": {
"admission_barrier": "active",
"dispatch_barrier": "active",
"worker_acknowledgement": "acknowledged",
"known_work": {
"pending": 0,
"cancellation_requested": 0,
"cancelled": 0,
"terminal": 0,
"unknown": 0,
"failed": 0
}
}
}

partially_enforced and failed also leave the hold active. Review pending, cancellation-requested, unknown or failed work with the responsible human; these states can change with later evidence. If the budget expires in accepted or enforcing, retain the intervention ID and arrange follow-up reads and operator investigation. A timeout never releases a hold. For this idle-agent exercise, proceed to recovery after enforced; investigate any other result rather than treating it as completion.

6. Release through human recovery and verify both states​

Once the incident is reviewed, an Owner, Admin or Security Admin opens Governance → Policies → External security → Interventions. Match the test agent, incident reference, reason and acceptance time with the saved response. Complete Release reason, enter the exact name under Confirm agent name, and choose Release this hold. Complete recent authentication when requested. If unresolved work exists, review it before selecting I acknowledge unresolved or unknown external outcomes. Failed cleanup requires the Retry enforcement action and successful cleanup before final release.

Read the intervention and the effective target state with the original service credential:

dt_security_request --output "$DT_WORKDIR/released.json" \
"$DT_ORIGIN$DT_STATUS_PATH"
jq '{id, state, hold_active, released_at}' "$DT_WORKDIR/released.json"

dt_security_request --output "$DT_WORKDIR/after.json" \
"$DT_ORIGIN$DT_CONTROL_PATH"
jq '{target, held, effective_state, active_intervention_ids, generation,
barriers, in_flight}' "$DT_WORKDIR/after.json"

jq -e '.state == "released" and .hold_active == false and .released_at != null' \
"$DT_WORKDIR/released.json"
jq -e '.held == false and .effective_state == "active"
and (.active_intervention_ids | length) == 0
and .barriers.admission == "inactive" and .barriers.dispatch == "inactive"' \
"$DT_WORKDIR/after.json"

The intervention projection should resemble:

{
"id": "7eeb0143-4498-4bd6-bdac-a5fe67e0d198",
"state": "released",
"hold_active": false,
"released_at": "2026-10-02T14:02:00+00:00"
}

For this single-hold exercise, the target projection should resemble the following. A first halt and final release move an initially zero generation to two; use your returned generation, since prior controls change it.

{
"target": {"type": "agent", "id": "8fe80423-34d1-4f17-95ad-aac7597cd201"},
"held": false,
"effective_state": "active",
"active_intervention_ids": [],
"generation": 2,
"barriers": {"admission": "inactive", "dispatch": "inactive"},
"in_flight": {
"known": 0,
"cancellation_requested": 0,
"cancelled": 0,
"terminal": 0,
"unknown": 0,
"failed": 0
}
}

If the target is still held, inspect active_intervention_ids and review the other holds; releasing this intervention does not release them. An open control can still report unknown external outcomes, which must remain in the incident record. Release does not resume cancelled or stale work. Any desired continuation is a deliberate fresh execution through normal admission and approval checks.

After verified recovery, archive the exact request, responses and identifiers in the incident record, then revoke this exercise's credential using Revoke credential in the identity section. Revocation does not release holds. Only then remove the local incident files and shell token:

unset DT_TOKEN DT_API_KEY
rm -rf -- "$DT_WORKDIR"
trap - EXIT
unset DT_WORKDIR

Recovery and rollout​

Only an authorized human in DT releases a specific intervention with reason and recent authentication; final release requires exact agent-name confirmation and acknowledgement of unresolved outcomes. Other holds remain effective. Release never replays cancelled work or restores grants/stale approvals; continuation is a deliberate fresh execution through normal admission checks.

Operators follow response deployment and acceptance. Shared work is counted once across holds, and unknown outcomes remain visible after release until a trusted receipt settles them. Validate worker, restart, dispatch and telemetry behavior for your exact release; the presence of these API routes alone is not proof that a deployment enforces a halt.