PlayOS Runtime IPC Specification

Authoritative repository: playos-runtime
Protocol version: 1
Cross-references: architecture.md §7.7, security-model.md

This document specifies the internal PlayOS IPC protocol. It is not a public application interface. Games and non-trusted clients must never connect to these endpoints.


Table of Contents

  1. Overview
  2. Transport
  3. Access Control
  4. Message Format
  5. Control IPC — control.sock
  6. Lifecycle Transport — per-game fd
  7. Compositor Control Channel
  8. Protocol Versioning
  9. Error Handling

1. Overview

playos-runtime owns the private integration layer between trusted PlayOS system components. It defines three distinct communication channels:

ChannelTransportDirectionPurpose
Control IPCUnix socketShell/overlay → playos-initGame launch, shutdown, factory reset, system commands
Lifecycle transportPipe fd per gameplayos-init → gameLifecycle events (foreground, background, terminate)
Compositor controlUnix socketplayos-init/runtime → compositorSet expected game, show/hide overlay, force game exit

Normal games never use these channels directly. They receive lifecycle events through playos_lifecycle_poll() from playos-platform-api, which reads from the lifecycle fd.


2. Transport

Control IPC socket

/run/playos/control.sock
Type: SOCK_SEQPACKET (message-boundaries preserved, reliable, ordered)
Owner: root:playos-trusted
Mode: 0660

Compositor control socket

/run/playos/compositor.sock
Type: SOCK_SEQPACKET
Owner: root:playos-trusted
Mode: 0660

Lifecycle fd

A write end of a pipe(2) passed to the game process as PLAYOS_LIFECYCLE_FD in its environment. The game process owns the read end; playos-init writes events to the write end.


3. Access Control

Only processes in the playos-trusted UNIX group may connect to control sockets.

ComponentGroup membership
playos-initroot (owns sockets)
playos-compositorplayos-trusted
playos-shellplayos-trusted
playos-overlayplayos-trusted
Active gameNot in playos-trusted — no socket access

The lifecycle fd is a one-directional pipe: the game can only read from it, never write to it or connect to any IPC socket.


4. Message Format

All messages use a simple length-prefixed binary frame:

+--------+--------+---...---+
| magic  | length |  body   |
| 4 bytes| 4 bytes| N bytes |
+--------+--------+---...---+
  • magic: 0x504C4F53 (PLOS in ASCII) — validates frame start
  • length: little-endian uint32, byte count of body only
  • body: JSON-encoded message (UTF-8, no trailing null)

Using JSON for the body makes messages human-readable for debugging while keeping the framing simple and versioned.

Maximum message size: 65536 bytes (64 KB)

Example message:

{
  "v": 1,
  "type": "LaunchGame",
  "game_id": "com.example.game",
  "manifest_path": "/data/games/com.example.game/manifest.json"
}

All messages include "v" (protocol version) and "type" fields.


5. Control IPC — control.sock

Trusted clients (shell, overlay) send requests; playos-init sends responses and async events.

Request → Response messages

LaunchGame

{
  "v": 1,
  "type": "LaunchGame",
  "game_id": "com.example.game",
  "manifest_path": "/data/games/com.example.game/manifest.json"
}

Response:

{ "v": 1, "type": "LaunchGameAck", "game_id": "com.example.game", "launch_token": "<uuid>" }
{ "v": 1, "type": "LaunchGameError", "game_id": "com.example.game", "reason": "already_running" }

Error reasons: already_running, invalid_manifest, executable_not_found, unsupported_api_version, permission_denied


TerminateGame

{ "v": 1, "type": "TerminateGame", "game_id": "com.example.game", "force": false }

force: true — skip cooperative SIGTERM and go straight to SIGKILL after 500ms.

Response:

{ "v": 1, "type": "TerminateGameAck", "game_id": "com.example.game" }

QueryStatus

{ "v": 1, "type": "QueryStatus" }

Response:

{
  "v": 1,
  "type": "StatusReport",
  "compositor_pid": 42,
  "compositor_state": "GAME_FOREGROUND",
  "game_pid": 123,
  "game_id": "com.example.game",
  "uptime_s": 3600
}

game_pid and game_id are null when no game is running.


Shutdown

{ "v": 1, "type": "Shutdown" }

playos-init delivers PLAYOS_LIFECYCLE_TERMINATE to the game, waits up to 2 seconds, then kills all processes, syncs filesystems, and calls reboot(RB_POWER_OFF).


Reboot

{ "v": 1, "type": "Reboot" }

Same as Shutdown but calls reboot(RB_AUTOBOOT).


FactoryReset

{
  "v": 1,
  "type": "FactoryReset",
  "erase_games": false,
  "erase_saves": false,
  "erase_cache": true,
  "erase_config": true,
  "erase_logs": false
}

Requires no active game. Erases selected /data/ subdirectories and recreates them.

Response:

{ "v": 1, "type": "FactoryResetComplete" }
{ "v": 1, "type": "FactoryResetError", "reason": "game_running" }

SetPerfProfile

{ "v": 1, "type": "SetPerfProfile", "profile": "balanced" }

profile values: "balanced", "power_save", "performance"

Response:

{ "v": 1, "type": "SetPerfProfile", "accepted": true }
{ "v": 1, "type": "SetPerfProfile", "accepted": false, "reason": "thermal_denied" }

reason values: thermal_denied, invalid_profile, epp_write_failed


Suspend

{ "v": 1, "type": "Suspend" }

Fire-and-forget. playos-init delivers PLAYOS_LIFECYCLE_SUSPEND to the active game, attempts S3 suspend (mem to /sys/power/state), then delivers PLAYOS_LIFECYCLE_RESUME after resume (or immediately on failure). No response is sent.


ApplyUpdate

{ "v": 1, "type": "ApplyUpdate", "path": "/data/updates/0.2.0.playosb" }

Requests that playos-init apply a system update bundle at path to the inactive slot. path must reside under /data/updates/ and carry the .playosb suffix. Exactly one update may be in flight at a time.

Response:

{ "v": 1, "type": "ApplyUpdateAck", "accepted": true }
{ "v": 1, "type": "ApplyUpdateError", "reason": "..." }

Error reasons: not_found, invalid_bundle, signature_invalid, update_in_progress, game_running, internal_error

Progress is reported via the async UpdateProgress / UpdateComplete / UpdateError events below.


Async events (init → client, unsolicited)

GameStarted

{
  "v": 1,
  "type": "GameStarted",
  "game_id": "com.example.game",
  "pid": 456,
  "launch_token": "<uuid>"
}

GameExited

{
  "v": 1,
  "type": "GameExited",
  "game_id": "com.example.game",
  "exit_code": 0
}

GameCrashed

{
  "v": 1,
  "type": "GameCrashed",
  "game_id": "com.example.game",
  "exit_code": 134,
  "signal": 6
}

ThermalStateChanged

{ "v": 1, "type": "ThermalStateChanged", "state": 2 }

state values (integer): 0 normal, 1 warm, 2 hot, 3 critical

PerfProfileChanged

{ "v": 1, "type": "PerfProfileChanged", "profile": 1 }

profile values (integer): 0 balanced, 1 power_save, 2 performance

UpdateProgress

{ "v": 1, "type": "UpdateProgress", "step": "verify", "percent": 25 }

step values: verify, write_inactive_slot, write_efi, update_boot_json, sync. percent is 0–100.

UpdateComplete

{ "v": 1, "type": "UpdateComplete", "active_slot": "b", "version": "0.2.0" }

Emitted after the inactive slot is written and boot.json is switched. The system requires a reboot to boot the new slot.

UpdateError

{ "v": 1, "type": "UpdateError", "step": "verify", "reason": "signature_invalid" }

Emitted when an update fails after being accepted. reason matches the ApplyUpdateError reason set.


6. Lifecycle Transport — per-game fd

playos-init passes PLAYOS_LIFECYCLE_FD=<n> in the game's environment. The fd is the read end of a pipe.

Each event is a single byte:

Byte valueEvent
0x00PLAYOS_LIFECYCLE_FOREGROUND
0x01PLAYOS_LIFECYCLE_BACKGROUND
0x02PLAYOS_LIFECYCLE_SUSPEND
0x03PLAYOS_LIFECYCLE_RESUME
0x04PLAYOS_LIFECYCLE_TERMINATE

On EOF (pipe write end closed): treated as TERMINATE.

playos_lifecycle_poll() in libplayos reads from this fd.


7. Compositor Control Channel

playos-init (and playos-runtime client library) communicates with playos-compositor via /run/playos/compositor.sock.

This channel uses the same framing as control IPC.

SetExpectedGame

{ "v": 1, "type": "SetExpectedGame", "launch_token": "<uuid>", "game_id": "com.example.game" }

Tells the compositor which Wayland client to expect. The compositor matches by checking the PLAYOS_LAUNCH_TOKEN environment variable of connecting clients.

ClearExpectedGame

{ "v": 1, "type": "ClearExpectedGame" }

ForceTerminateGame

{ "v": 1, "type": "ForceTerminateGame" }

Compositor destroys the game surface immediately (for crash recovery). playos-init handles the actual process kill.

ShowOverlay

{ "v": 1, "type": "ShowOverlay" }

HideOverlay

{ "v": 1, "type": "HideOverlay" }

Compositor → init events

GameSurfaceReady

{ "v": 1, "type": "GameSurfaceReady", "launch_token": "<uuid>" }

Emitted when the game commits its first valid buffer. playos-init records this as a successful launch.

CompositorStateChanged

{ "v": 1, "type": "CompositorStateChanged", "state": "GAME_FOREGROUND" }

state values: SHELL_FOREGROUND, GAME_STARTING, GAME_FOREGROUND, PLAYOS_UI_FOREGROUND_WITH_GAME_BACKGROUND, TERMINATING_GAME


8. Protocol Versioning

All messages include "v": <version_integer>. The current version is 1.

On version mismatch:

{ "v": 1, "type": "ProtocolError", "reason": "version_mismatch", "supported": [1] }

The receiver closes the connection after sending this error.

Backward compatibility: A server implementing version N must also accept messages with "v": M where M < N, treating unknown fields as ignored. It must not accept "v": M where M > N.


9. Error Handling

All request messages may receive a generic error response:

{ "v": 1, "type": "Error", "reason": "internal_error", "message": "..." }

reason values: version_mismatch, invalid_message, permission_denied, internal_error, not_implemented

Connection loss: If playos-init loses a trusted client connection unexpectedly, it logs the event. This does not affect system operation. Clients should reconnect with exponential backoff.

Rate limiting: playos-init may reject rapid repeated requests (e.g., rapid LaunchGame calls) with reason: "rate_limited".