Skip to content

Approve one reviewed disruptive host-policy group and wait for idle (RH05).

POST
/v1/admin/hosts/{id}/idle-apply
curl --request POST \
--url http://localhost:8080/v1/admin/hosts/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/idle-apply \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "group": "example", "expected_revision": "example", "content_sha256": "example", "prerequisites_sha256": "example", "prerequisites": [ { "kind": "example", "id": "example" } ], "approval_boot_incarnation": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "approval_review_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "expires_at": "2026-04-15T12:00:00Z" }'

Binds the reviewed revision, content, resolved values and prerequisite facts to this control-plane boot and the current agent connection at grant time. Acquires only this attempt’s admission restriction. Waiting never terminates a session; missing or uncertain session/preparation inventory never establishes idle. An identical replay in waiting or offered with the same full request, including expires_at, returns 202 with the same attempt_id and current phase. A replay in cancel_pending or revoked_unstarted, after expiry, or after a control-plane boot returns approval_superseded. A replay during accepted, activating, awaiting_startup, verifying, failed before recovery is decided, recovery_verifying, recovery_awaiting_startup or uncertain returns attempt_conflict with that attempt. A replay after applied, recovered, resolved uncertain or terminal failed returns approval_superseded. Neither replay revives an old attempt. The request’s boot incarnation and approval_review_id fence a delayed retry even when policy content is unchanged. A request with expires_at in the past also returns approval_superseded without a hold. A same-boot unoffered attempt expired by time can become revoked_unstarted and release only its own restriction. After a process boot, even a restored approved row becomes cancel_pending and stays protected until complete authenticated current-connection inventory proves no journal record under its ID. The approval ID is the public attempt_id; at offer the server inserts an offered attempt with that ID and changes the approval to offered atomically before sending. Authenticated acceptance advances both rows together. Confirmed nonacceptance terminalizes the offered attempt and approval together; a restored accepted journal record reconstructs a missing attempt under the approval ID and advances a restored cancel_pending approval to accepted in the same transaction before the inventory gate opens; the attempt takes the authenticated journal phase and sequence. The approval decides public phase until acceptance or terminal nonacceptance; the attempt decides thereafter. A same-boot reconnect revokes unoffered waiting locally and moves offered to cancel_pending. A current review ID with a different body while a live approval exists returns attempt_conflict with current. Host-wide unique-index conflicts also return attempt_conflict. A terminal uncertain attempt with an unresolved protective restriction blocks grants. Every restart-group review token rotates when an approval exits approved or offered for cancel_pending or a terminal state, or exits cancel_pending for a terminal state; an accepted restart attempt reaches a terminal outcome, an unresolved disruptive admission hold resolves, connection/journal authority changes, or complete authenticated reconciliation opens disruptive availability. Unrelated safe next-session edits, ordinary sessions and unrelated owner holds preserve it; rotation alone does not revoke another live approval. An offered attempt becomes cancel_pending and stays protected until authenticated nonacceptance or complete journal inventory proves no acceptance. Only then does it become revoked_unstarted; an accepted journal record instead resumes accepted or later execution. Therefore revoked_unstarted always means started false and admission_restricted false. Neither expiry nor boot ends a session. A grant from an old boot or agent connection is rejected. An open disruptive attempt includes waiting, offered, cancel_pending, accepted, activating, awaiting_startup, verifying, failed while recovery remains possible, recovery_verifying, recovery_awaiting_startup and uncertain while protection is unresolved. Normative behavior: control-api.md section “Idle apply, cancellation and recovery”.

id
required
string format: uuid
Media typeapplication/json
object
group
required
string
>= 1 characters
expected_revision
required

Canonical nonnegative decimal string; no sign, whitespace or leading zeros.

string
/^(0|[1-9][0-9]*)$/
content_sha256
required

Lowercase SHA-256 hex digest of canonical content.

string
/^[0-9a-f]{64}$/
prerequisites_sha256
required

Lowercase SHA-256 hex digest of canonical content.

string
/^[0-9a-f]{64}$/
prerequisites
required

Exact group-specific facts sorted bytewise by (kind,id): agent image digest, driver, accessible device, passing probe IDs and last verified group digest as relevant, plus the mandatory accepted_attempts set digest for restart scope. Digest input is each UTF-8 kind, NUL, id, LF. NUL and LF are forbidden within either field. Agent rechecks these facts before durable acceptance.

Array<object>
object
kind
required
string
/^[^\x00\n]+$/
id
required
string
/^[^\x00\n]+$/
approval_boot_incarnation
required

Must match the reviewed preview and persisted current process boot.

string format: uuid
approval_review_id
required

Must match the current server-issued token for a new grant; all restart-group IDs on the host rotate when an approval exits approved or offered for cancel_pending or a terminal state or exits cancel_pending for a terminal state

string format: uuid
expires_at
required
string format: date-time
Examplegenerated
{
"group": "example",
"expected_revision": "example",
"content_sha256": "example",
"prerequisites_sha256": "example",
"prerequisites": [
{
"kind": "example",
"id": "example"
}
],
"approval_boot_incarnation": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"approval_review_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"expires_at": "2026-04-15T12:00:00Z"
}

Approval saved or identically replayed; current phase waiting or offered. This is not application proof.

Media typeapplication/json
object
attempt_id
required
string format: uuid
group
required
string
revision
required

Canonical nonnegative decimal string; no sign, whitespace or leading zeros.

string
/^(0|[1-9][0-9]*)$/
content_sha256
required

Lowercase SHA-256 hex digest of canonical content.

string
/^[0-9a-f]{64}$/
prerequisites_sha256
required

Lowercase SHA-256 hex digest of canonical content.

string
/^[0-9a-f]{64}$/
phase
required
string
Allowed values: waiting offered accepted activating awaiting_startup verifying applied failed recovery_verifying recovery_awaiting_startup recovered uncertain released cancel_pending revoked_unstarted
started
required

True only after durable agent acceptance is proven; unknown acceptance retains the admission hold.

boolean
admission_restricted
required
boolean
remedy
string | null
next_retry_at
string | null format: date-time
recovery_release
One of:
object
state
required
string
Allowed values: pending released
requested_at
required

Null only for an attempt reconciled as released without a recorded request.

string | null format: date-time
waiting_for
required

Null once released.

string | null
Allowed values: agent_connection agent_upgrade agent_report
Example
{
"phase": "waiting",
"recovery_release": {
"state": "pending",
"waiting_for": "agent_connection"
}
}

Malformed or invalid request.

Media typeapplication/json
object
error
required
object
code
required

E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, host_not_ready, restart_required, stale_revision, unsupported_source, approval_superseded, approval_expired, attempt_conflict, cancel_too_late, retry_exhausted, home_conflict, placement_ineligible, inherited_placement, recovery_uncertain, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Also omitted when the holder is the caller’s parked session or released runtime (amendment 39). Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.

string format: uuid
derived_tiles

Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.

Array<object>
object
id
required
string format: uuid
name
required
string
reason

Amendment 40 (#602), ADDITIVE: a machine-readable cause under code. On 503 no_host_available, home_host_unavailable: the launch is pinned to the host that holds the caller’s home for this app (Amendment 53: or it is pinned to the host of the caller’s Steam home for a sharing launch) and that host is offline or no longer enrolled. Only an admin clears it, so a client does not spend its automatic retry on it. Names no host. Amendment 53 (#1001), on 409 home_in_use: steam_library_open_in_desktop (the holder mounts the Steam library as a shared home) or steam_game_running (the refused launch shares the library and a Steam session holds it); absent when the holder is already stopping. Open string: a client treats an unknown value as absent.

string
Examplegenerated
{
"error": {
"code": "example",
"message": "example",
"live_sessions": 1,
"session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"derived_tiles": [
{
"id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"name": "example"
}
],
"reason": "example"
}
}

Missing/invalid/expired/revoked token.

Media typeapplication/json
object
error
required
object
code
required

E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, host_not_ready, restart_required, stale_revision, unsupported_source, approval_superseded, approval_expired, attempt_conflict, cancel_too_late, retry_exhausted, home_conflict, placement_ineligible, inherited_placement, recovery_uncertain, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Also omitted when the holder is the caller’s parked session or released runtime (amendment 39). Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.

string format: uuid
derived_tiles

Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.

Array<object>
object
id
required
string format: uuid
name
required
string
reason

Amendment 40 (#602), ADDITIVE: a machine-readable cause under code. On 503 no_host_available, home_host_unavailable: the launch is pinned to the host that holds the caller’s home for this app (Amendment 53: or it is pinned to the host of the caller’s Steam home for a sharing launch) and that host is offline or no longer enrolled. Only an admin clears it, so a client does not spend its automatic retry on it. Names no host. Amendment 53 (#1001), on 409 home_in_use: steam_library_open_in_desktop (the holder mounts the Steam library as a shared home) or steam_game_running (the refused launch shares the library and a Steam session holds it); absent when the holder is already stopping. Open string: a client treats an unknown value as absent.

string
Examplegenerated
{
"error": {
"code": "example",
"message": "example",
"live_sessions": 1,
"session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"derived_tiles": [
{
"id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"name": "example"
}
],
"reason": "example"
}
}

Authenticated but insufficient role / not the owner (precedes resource lookup).

Media typeapplication/json
object
error
required
object
code
required

E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, host_not_ready, restart_required, stale_revision, unsupported_source, approval_superseded, approval_expired, attempt_conflict, cancel_too_late, retry_exhausted, home_conflict, placement_ineligible, inherited_placement, recovery_uncertain, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Also omitted when the holder is the caller’s parked session or released runtime (amendment 39). Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.

string format: uuid
derived_tiles

Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.

Array<object>
object
id
required
string format: uuid
name
required
string
reason

Amendment 40 (#602), ADDITIVE: a machine-readable cause under code. On 503 no_host_available, home_host_unavailable: the launch is pinned to the host that holds the caller’s home for this app (Amendment 53: or it is pinned to the host of the caller’s Steam home for a sharing launch) and that host is offline or no longer enrolled. Only an admin clears it, so a client does not spend its automatic retry on it. Names no host. Amendment 53 (#1001), on 409 home_in_use: steam_library_open_in_desktop (the holder mounts the Steam library as a shared home) or steam_game_running (the refused launch shares the library and a Steam session holds it); absent when the holder is already stopping. Open string: a client treats an unknown value as absent.

string
Examplegenerated
{
"error": {
"code": "example",
"message": "example",
"live_sessions": 1,
"session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"derived_tiles": [
{
"id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"name": "example"
}
],
"reason": "example"
}
}

No such resource.

Media typeapplication/json
object
error
required
object
code
required

E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, host_not_ready, restart_required, stale_revision, unsupported_source, approval_superseded, approval_expired, attempt_conflict, cancel_too_late, retry_exhausted, home_conflict, placement_ineligible, inherited_placement, recovery_uncertain, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Also omitted when the holder is the caller’s parked session or released runtime (amendment 39). Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.

string format: uuid
derived_tiles

Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.

Array<object>
object
id
required
string format: uuid
name
required
string
reason

Amendment 40 (#602), ADDITIVE: a machine-readable cause under code. On 503 no_host_available, home_host_unavailable: the launch is pinned to the host that holds the caller’s home for this app (Amendment 53: or it is pinned to the host of the caller’s Steam home for a sharing launch) and that host is offline or no longer enrolled. Only an admin clears it, so a client does not spend its automatic retry on it. Names no host. Amendment 53 (#1001), on 409 home_in_use: steam_library_open_in_desktop (the holder mounts the Steam library as a shared home) or steam_game_running (the refused launch shares the library and a Steam session holds it); absent when the holder is already stopping. Open string: a client treats an unknown value as absent.

string
Examplegenerated
{
"error": {
"code": "example",
"message": "example",
"live_sessions": 1,
"session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"derived_tiles": [
{
"id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"name": "example"
}
],
"reason": "example"
}
}

Check boot token and request expiry first. Identical waiting/offered replay returns 202; replay in cancel_pending/revoked_unstarted returns approval_superseded; any other open disruptive phase returns attempt_conflict with current. A new grant then locks and checks the review token, rechecks availability and inserts atomically. approval_superseded has ErrorEnvelope and requires a fresh review.

Media typeapplication/json
Any of:
object
error
required
object
code
required

E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, host_not_ready, restart_required, stale_revision, unsupported_source, approval_superseded, approval_expired, attempt_conflict, cancel_too_late, retry_exhausted, home_conflict, placement_ineligible, inherited_placement, recovery_uncertain, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Also omitted when the holder is the caller’s parked session or released runtime (amendment 39). Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.

string format: uuid
derived_tiles

Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.

Array<object>
object
id
required
string format: uuid
name
required
string
reason

Amendment 40 (#602), ADDITIVE: a machine-readable cause under code. On 503 no_host_available, home_host_unavailable: the launch is pinned to the host that holds the caller’s home for this app (Amendment 53: or it is pinned to the host of the caller’s Steam home for a sharing launch) and that host is offline or no longer enrolled. Only an admin clears it, so a client does not spend its automatic retry on it. Names no host. Amendment 53 (#1001), on 409 home_in_use: steam_library_open_in_desktop (the holder mounts the Steam library as a shared home) or steam_game_running (the refused launch shares the library and a Steam session holds it); absent when the holder is already stopping. Open string: a client treats an unknown value as absent.

string
current
required
object
attempt_id
required
string format: uuid
group
required
string
revision
required

Canonical nonnegative decimal string; no sign, whitespace or leading zeros.

string
/^(0|[1-9][0-9]*)$/
content_sha256
required

Lowercase SHA-256 hex digest of canonical content.

string
/^[0-9a-f]{64}$/
prerequisites_sha256
required

Lowercase SHA-256 hex digest of canonical content.

string
/^[0-9a-f]{64}$/
phase
required
string
Allowed values: waiting offered accepted activating awaiting_startup verifying applied failed recovery_verifying recovery_awaiting_startup recovered uncertain released cancel_pending revoked_unstarted
started
required

True only after durable agent acceptance is proven; unknown acceptance retains the admission hold.

boolean
admission_restricted
required
boolean
remedy
string | null
next_retry_at
string | null format: date-time
recovery_release
One of:
object
state
required
string
Allowed values: pending released
requested_at
required

Null only for an attempt reconciled as released without a recorded request.

string | null format: date-time
waiting_for
required

Null once released.

string | null
Allowed values: agent_connection agent_upgrade agent_report
Example
{
"current": {
"phase": "waiting",
"recovery_release": {
"state": "pending",
"waiting_for": "agent_connection"
}
}
}