Ignore or un-ignore one appid (one route, two directions).
const url = 'http://localhost:8080/v1/admin/apps/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/library/rules/example';const options = { method: 'PUT', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"rule":"ignore","note":"example","external_source":"steam"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}use std::str::FromStr;use serde_json::json;use reqwest;
#[tokio::main]pub async fn main() { let url = "http://localhost:8080/v1/admin/apps/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/library/rules/example";
let payload = json!({ "rule": "ignore", "note": "example", "external_source": "steam" });
let mut headers = reqwest::header::HeaderMap::new(); headers.insert("Authorization", "Bearer <token>".parse().unwrap()); headers.insert("Content-Type", "application/json".parse().unwrap());
let client = reqwest::Client::new(); let response = client.request(reqwest::Method::from_str("PUT").unwrap(), url) .headers(headers) .json(&payload) .send() .await;
let results = response.unwrap() .json::<serde_json::Value>() .await .unwrap();
dbg!(results);}curl --request PUT \ --url http://localhost:8080/v1/admin/apps/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/library/rules/example \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "rule": "ignore", "note": "example", "external_source": "steam" }'Phase 4. rule:“ignore” is the Ignore action and it does THREE things in one transaction, NONE OF THEM A DELETE - writes the rule row, sets enabled=false on the tile if one exists, and revokes that tile’s granted_by=‘provider’ entitlements FLEET-WIDE (an Ignore is a fleet decision; a half-revoke would leave the tile live for everyone who happens not to be scanned next). It is not a delete because apps cascades to user_app_favourites and app_artwork, so deleting a junk tile destroys every user’s favourite of it and its artwork row irreversibly - and the next scan re-creates a bare row anyway, because the appid is still on disk. Durability rests on two INDEPENDENT facts, deliberately, because either alone is a resurrection bug: the rule row makes the reconciler skip the appid, AND the reconciler never modifies an existing tile, so enabled=false is never flipped back. rule:“allow” outranks the built-in denylist and writes ONLY the rule row - it does NOT re-enable an existing disabled tile; the next scan republishes the appid through the ladder. That asymmetry is intentional: a tile may have been disabled by hand for a reason unrelated to discovery, and an un-ignore must not override the admin who did it. The primary key (parent_app_id, external_source, external_id) is the idempotency key, so a repeat is a REPLACE and never an accumulation. Audited as app.library.rule.set. The 400 on this route now covers TWO causes: a malformed appid, and a real {id} that is not a library provider - the latter checked INSIDE the write transaction, so a concurrent un-marking of the provider cannot slip a rule in behind it.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Steam library discovery Phase 4: the provider title id an appid rule applies to - today a Steam appid. VALIDATED AT THE HANDLER AS WELL AS BY THE DATABASE CHECK, deliberately: this table is ADMIN-WRITABLE and takes the value straight from a URL, and the value ends up in STEAM_STARTUP_FLAGS, which the quasar-steam entrypoint word-splits with read -r -a. The handler is what makes a bad value a 400 instead of a 500; the CHECK is the durable guard and the only one of the two that survives an admin editing the row directly later. Bounded below 2^32 in addition to the pattern.
Request Bodyrequired
Section titled “Request Bodyrequired”object
‘ignore’ suppresses an appid the built-in denylist does not know about; ‘allow’ un-suppresses a game it wrongly caught, and OUTRANKS the built-in list - rung 1 of the ladder sits above rung 3.
Optional. Truncated at 512 characters.
Optional, defaults to ‘steam’; any other value is 400 validation_failed. The vocabulary is provider-shaped so Heroic or RomM slot in later with no re-model, but only ‘steam’ exists today.
Responses
Section titled “Responses”Rule written.
NOTE THE SHAPE ASYMMETRY, which is real and is recorded rather than smoothed over: rule on the REQUEST is the two-value string, and rule here is the whole stored rule object (whose own rule field is that string).
object
One operator-written layer-2 rule. Layer 1 is a shipped code constant (an appid set plus case-insensitive Valve tool name prefixes) and UPDATING IT IS A CODE CHANGE AND A RELEASE - which is exactly why this layer exists, because an operator cannot wait for one.
object
‘ignore’ suppresses an appid the built-in denylist does not know about; ‘allow’ un-suppresses a game it wrongly caught, and OUTRANKS the built-in list - rung 1 of the ladder sits above rung 3.
Why, for the next operator. Truncated at 512 characters.
The acting admin, resolved server-side from the bearer identity. NULL once that account is deleted (ON DELETE SET NULL, not CASCADE) - deleting the operator who wrote a rule must not silently un-suppress every appid they ignored.
Set, and re-set on a replace.
Whether this call disabled a tile. Always false for an ‘allow’, which by design changes nothing but the rule row - it does NOT re-enable an existing disabled tile, because that tile may have been disabled by hand for a reason unrelated to discovery; the next scan republishes the appid through the ladder instead.
Provider entitlements removed by this call - FLEET-WIDE, because an Ignore is a fleet decision and a half-revoke would leave the tile live for everyone who happens not to be scanned next. granted_by=‘admin’ rows are never touched.
Example
{ "rule": { "external_source": "steam", "rule": "ignore" }}Malformed or invalid request.
object
object
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, restart_required, 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.
Present on restart_required.
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. 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.
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.
object
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" } ] }}Missing/invalid/expired/revoked token.
object
object
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, restart_required, 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.
Present on restart_required.
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. 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.
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.
object
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" } ] }}Authenticated but insufficient role / not the owner (precedes resource lookup).
object
object
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, restart_required, 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.
Present on restart_required.
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. 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.
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.
object
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" } ] }}No such resource.
object
object
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, restart_required, 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.
Present on restart_required.
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. 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.
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.
object
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" } ] }}