Skip to content

Development-agent protocol

The resident agent is a manually started, read-only service. USB discovery identifies a connected phone and can stage its SIS package; it does not expose an agent socket. The emulator profile listens on 127.0.0.1:39101 with a public test key. A phone-specific build embeds a separate 32-byte key, sends keyed UDP discovery probes on the local network and connects to a responding console at TCP port 39103. No host or phone IP address is stored in its SIS.

The agent uses Mbed TLS crypto primitives for random challenges and HMAC-SHA256, without a TLS connection. The SDK's Symbian::Tls target and project-local CA bundle remain available to applications separately. This agent protocol authenticates both peers but does not encrypt traffic; use it on a trusted local network. Neither USB detection nor a completed ARM build proves installation or execution on a Nokia 808.

WebSocket transport

Agent 1.1 uses a binary WebSocket over HTTP/2 prior knowledge, negotiated through RFC 8441 extended CONNECT /symbian-agent. Both endpoints use nghttp2 1.70.0. The phone is the WebSocket client on an outbound connection; the console accepts it as the WebSocket server. For emulator loopback, the host connects as client and the guest accepts as server. The authentication roles remain guest as challenge issuer and host as proof responder.

The SDK exposes Symbian::WebSocket, WebSocketStream, WebSocketServer, and a socket-independent symbian::websocket::WebSocket codec. Host Python uses symbian.websocket.WebSocketStream and WebSocketServer, with the same native codec through _native.WebSocketCodec. Native bindings release the GIL and retain no Python callbacks. The worker and its existing A11 thread executor remain responsible for ownership and scheduling.

The framing parser, endian helpers, masking loop and frame writer come from A11's Http2WebSocketChannel. The parser keeps A11's buffer adoption path. The transport adapter replaces A11's unavailable libuv HTTP body stream with nghttp2 memory callbacks and SDK TCP calls. Guest masking keys use the existing entropy adapter; host keys use OpenSSL. The default message limit is 4100 bytes (4 KiB control plus its prefix); each direction has a 64 KiB queue bound, and receive queues hold at most 16 messages. Headers are capped at 2048 bytes and 16 fields. nghttp2 also bounds settings, acknowledgements and continuation frames and disables dynamic HPACK tables. These are queue limits, not an attestation of total process memory usage.

Authentication packets and complete control frames travel in binary messages. Message boundaries do not replace the native control length prefix. Fragmented binary messages and ping/pong use A11's framing code. Close drains already received messages; transport/protocol errors abort the connection. Handshake and request deadlines are distinct from stream lifetime.

This endpoint requires RFC 8441 support; HTTP/1.1 Upgrade clients and browser WebSocket APIs cannot directly use this cleartext HTTP/2 endpoint. The old raw-TCP agent and the new host require matching transport versions. Physical 1.0.8 observations do not establish compatibility of the new transport.

Pairing and authentication

The console creates one private key for each serial-derived USB identity anchor. It stores the key under ~/.local/share/symbian/agent-identities/ with owner-only permissions, outside the repository. The key is embedded in a phone-specific build. The handset panel displays an eight-character code derived from the key. The owner must compare that code with the console's card before checking live status. An authenticated reply proves possession of the key; the visual comparison connects that key to the phone in hand.

During a status check, the console listens on UDP port 39104 and TCP port 39103. The phone broadcasts SAGD1, a fresh eight-byte nonce and HMAC-SHA256(key, "symbian-agent-discover-v1" || nonce). The console replies only after checking the MAC; its reply is SAGR1, the same nonce and HMAC-SHA256(key, "symbian-agent-offer-v1" || nonce). The phone checks the entire reply and connects to its IPv4 sender. The console accepts a TCP peer only if it sent a valid discovery request during this check. Discovery and TCP ports are protocol constants; addresses are found anew on each status check.

After the WebSocket handshake and before reading a control frame, the guest sends SAG1 and a fresh 32-byte nonce. The host replies with a fresh 32-byte nonce and HMAC-SHA256(key, "symbian-agent-client-v1" || server_nonce || client_nonce). The guest checks the MAC, then returns HMAC-SHA256(key, "symbian-agent-server-v1" || server_nonce || client_nonce). The host checks the final proof before it sends hello. A failed or incomplete exchange closes the connection. A five-second deadline bounds the exchange. The guest requires secure entropy for fresh nonces. The RM-807 adapter needs the matching patched emulator; a physical target needs its own secure source.

The checked-in agent_service/test-agent.key is public and emulator-only. The build refuses to expose that key on Wi-Fi. A private build cannot use it. No key is sent over USB discovery or the agent socket.

Control session

Each control frame starts with a four-byte unsigned length in network byte order and exactly that many MessagePack bytes. Zero and lengths above 4 KiB are rejected before the payload is read. The first authenticated request must be hello. Any other request sent first, or a repeated hello, closes the session. The hello result declares protocol version 1, the 4 KiB limit, a cap of 16 requests per connection, and the available status, logs and workspace-list operations. Hello consumes one request slot.

The service applies one five-second Abseil deadline to each exchange's prefix, payload and response. The host session uses one response deadline even if a peer sends fragments slowly. An active-object listener passes accepted sockets to a bounded SDK worker in the emulator profile. The phone profile uses that worker for bounded UDP discovery and outbound TCP connection attempts. The service does not offer file writes, command execution, flashing or recovery operations.

The guest Symbian::Agent target owns the control codec in symbian/agent/guest_control.h. It accepts version-one hello/status with an empty body, or logs and workspace listing with exactly unsigned after and limit fields; limit must be 1–8. Unknown top-level fields survive a parse/encode cycle. The codec validates MessagePack after the socket owner checks framing and authentication. It does not own the listener, permission policy or scheduler. The C++ declarations and return types are in the native reference.

A status response names the service and ready state. A successful native tick query adds system.tick_count and system.tick_period_us; a successful primary display query adds display.width_pixels and display.height_pixels. Missing observations stay absent. The tick counter wraps and is an elapsed-time source; display geometry may differ from Window Server layout.

Service-local event log

AgentLogRing keeps 32 fixed-size process-local events. Logs reads return up to eight records after a sequence cursor, a next_cursor, and gap=true if older records were overwritten. Codes are 1 for authentication, 2 for a status read, 3 for a rejected frame and 4 for session closure. Each record includes a severity and clamped microseconds since this process created the ring. The times are not UTC and cannot be compared across process restarts; clock adjustments can affect elapsed intervals.

Agent workspace

workspace-list enumerates only the agent's own C:\private\e0000a31\workspace\ directory. The request cannot supply a path. Each page contains at most eight immediate child names, directory and read-only flags, byte sizes, a next_offset, and a more flag. The listing stops at 256 entries; offsets at or beyond that bound are rejected. A missing workspace is empty. Pages are not a snapshot, so files changed during pagination can shift their offsets. The agent does not read file contents or expose arbitrary device paths.

Host API and CLI

ReadOnlyAgentSession uses native bindings for MessagePack framing and typed Python models for results. The direct connection API and CLI serve the emulator listener. Pass its test key:

from pathlib import Path
from symbian.agent import ReadOnlyAgentSession

with ReadOnlyAgentSession.connect(
    "127.0.0.1", 39101, key_file=Path("agent_service/test-agent.key")
) as agent:
    print(agent.status())
    page = agent.logs(after=0, limit=8)
    print(page.records, page.next_cursor, page.gap)
    workspace = agent.workspace_list(after=0, limit=8)
    print(workspace.entries, workspace.next_offset, workspace.more)

The equivalent CLI commands are:

symbian agent hello 127.0.0.1 39101 --key-file agent_service/test-agent.key
symbian agent status 127.0.0.1 39101 --key-file agent_service/test-agent.key
symbian agent logs 127.0.0.1 39101 --key-file agent_service/test-agent.key \
  --after 0 --limit 8
symbian agent files 127.0.0.1 39101 --key-file agent_service/test-agent.key \
  --after 0 --limit 8

For a phone, the desktop console's Check live status opens temporary discovery and TCP listeners. Code using the host API directly can call ReadOnlyAgentSession.accept("0.0.0.0", 39103, key_file=key) on a trusted local network. The equivalent CLI command is symbian agent listen --key-file /private/agent.key; it discovers the phone without an IP argument. The public test key must never be used for a phone profile. The emulator guide gives the build and launch steps.

The console offers a separate Read service events action after a successful status check. It reads the newest eight events from the agent's fixed ring only when requested. The host API exposes recent_logs(limit=8) for this bounded snapshot; logs(after=cursor) remains available for cursor-based reads.

The phone-initiated CLI path can read the same log without an IP address:

symbian agent listen --key-file /private/agent.key --logs
symbian agent listen --key-file /private/agent.key --logs --after 12 --limit 8
symbian agent listen --key-file /private/agent.key --files

Each command opens one temporary authenticated listener. --after is a process-local sequence cursor; it does not survive an agent restart.

Device requirements

The emulator profile uses loopback and a public test key. For a physical device, use a private key and package, complete the pairing-code comparison, and check the device's network reachability and installation policy. The emulator key and transport fixture are unsuitable for deployment.