Mint a throwaway, auto-reaped ephemeral identity + token for automated UI validation (dev-gated;
const url = 'http://localhost:8080/v1/dev/agent-session';const options = { method: 'POST', headers: {'X-Quasar-Dev-Key': 'example', 'Content-Type': 'application/json'}, body: '{"role":"user","ttl_seconds":1800}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}use serde_json::json;use reqwest;
#[tokio::main]pub async fn main() { let url = "http://localhost:8080/v1/dev/agent-session";
let payload = json!({ "role": "user", "ttl_seconds": 1800 });
let mut headers = reqwest::header::HeaderMap::new(); headers.insert("X-Quasar-Dev-Key", "example".parse().unwrap()); headers.insert("Content-Type", "application/json".parse().unwrap());
let client = reqwest::Client::new(); let response = client.post(url) .headers(headers) .json(&payload) .send() .await;
let results = response.unwrap() .json::<serde_json::Value>() .await .unwrap();
dbg!(results);}curl --request POST \ --url http://localhost:8080/v1/dev/agent-session \ --header 'Content-Type: application/json' \ --header 'X-Quasar-Dev-Key: example' \ --data '{ "role": "user", "ttl_seconds": 1800 }'Registered ONLY when QUASAR_DEV_AGENT_AUTH=1; absent the flag the route
does not exist (404), and the control plane refuses to boot with the flag
set while QUASAR_ENV=production. Auth is the per-boot dev key (written to
the container log and /run/quasar/dev-agent-key), not a bearer token.
Response is shape-compatible with POST /v1/auth/login so every existing
client path works unchanged. The minted identity is a REAL user row with a
real token — server-side role enforcement is untouched — and is deleted
(with its sessions and device bindings) when its TTL lapses.
Parameters
Section titled “Parameters”Header Parameters
Section titled “Header Parameters”The per-boot dev secret. Wrong or missing → 401 with no message/timing distinction.
Request Body
Section titled “Request Body”object
Role=admin mints a REAL admin (same enforcement path); the server logs it at WARN with the source address.
Identity lifetime. Default 30 min, hard cap 8 h. The token TTL is clamped to this — a token cannot outlive its user.
Responses
Section titled “Responses”Ephemeral identity minted (login-shape + storage_keys).
object
The only credential ever returned — the identity’s password is random and never exposed.
Token AND identity expiry; the reaper deletes the user at/after this instant.
object
Present on /v1/me + register; omitted from the login user.
Convenience: the three web-SPA localStorage entries (quasar.auth.token / quasar.auth.expires_at / quasar.auth.user) ready to inject.
object
Example
{ "token_type": "Bearer", "user": { "role": "user" }}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" } ] }}