DDR-0036: E27 Session Public API Contract (DDR-0034)

Scope
- Session is the transport + framing + HELLO bootstrap + inbound event dispatch layer.
- Session does NOT perform linking (API_LINK) and does NOT perform authentication (authorize/elevate).
- Session does NOT orchestrate multi-step workflows or implement application policy.

Objects

SessionConfig
- host: str
- port: int
- connect_timeout_s: float
- recv_timeout_s: float
- recv_max_bytes: int
- (plus any TLS/socket options already present in config; Session treats these as transport-level only)

Identity
- client_identity: str (client identity string used by HELLO)

SessionInfo (read-only once established)
- session_id: str
- session_key_hex: str
- session_hmac_hex: str
- Established by HELLO and required for subsequent schema-0 encryption/decryption.

SessionState (lifecycle)
- DISCONNECTED: no socket resources owned
- CONNECTING: establishing TCP transport
- HELLO: transport established, HELLO in progress
- ACTIVE: ready for send/recv framed schema-0 messages
- CLOSED/FAILED (if present): terminal-ish state; may be represented as DISCONNECTED + last_error depending on implementation

Session class

Construction
Session(cfg: SessionConfig, *, client_identity: str, link_key_hex: str, logger: logging.Logger|None = None)

Inputs:
- cfg: transport/framing configuration
- client_identity: HELLO client identity string
- link_key_hex: provisioning-derived link key; used ONLY to decrypt HELLO response material (session keys)

Side-effects:
- Constructor does not open sockets and does not contact the panel.

Attributes (public, read-only usage expected)
- state: SessionState
- info: SessionInfo | None  (None until HELLO succeeds)
- on_connected: Callable[[], None] | None
- on_disconnected: Callable[[Exception|None], None] | None
- on_message: Callable[[dict], None] | None

Notes on callbacks:
- Callbacks are invoked synchronously from the method that detects the condition (typically connect()/pump_once()/recv_json()).
- Callbacks must be fast and non-blocking. If you need slow work, enqueue and return.

Lifecycle methods

connect() -> None
- Establishes TCP transport.
- Performs HELLO immediately after transport connect.
- On success:
  - state becomes ACTIVE
  - info becomes non-None with negotiated session parameters
  - framing deframe state is initialized/reset
  - on_connected() is invoked if provided
- On failure:
  - raises a SessionIOError or SessionProtocolError (see Errors)
  - ensures resources are closed (state returns to DISCONNECTED)
  - on_disconnected(err) may be invoked depending on failure timing (implementation-defined, but should be consistent)

close(*, reason: str|None = None) -> None
- Idempotent.
- Closes socket and clears readiness-critical resources (sock/deframe state may be cleared; info may be cleared or retained as implementation-defined).
- state becomes DISCONNECTED.
- on_disconnected(None) may be invoked for a clean close (implementation-defined; prefer invoking to simplify consumers).

reconnect() -> None (mechanical convenience, no policy)
- Equivalent to: close(); connect()
- Does not include backoff, retry loops, or credential refresh. Those are higher-layer concerns.

Messaging methods

send_json(obj: dict, *, protocol_base: int = 0x80) -> None
Preconditions:
- Session must be ACTIVE and have info/session keys established.
Behavior:
- Serializes obj as compact UTF-8 JSON (separators: "," and ":").
- Encrypts payload into schema-0 envelope using session_key_hex and protocol_base.
- Frames encrypted envelope using canonical framing (frame_build).
- Writes bytes to socket (sendall semantics).
Errors:
- SessionNotReadyError if not ACTIVE/ready.
- SessionIOError for socket write errors (broken pipe, timeout, etc).
- SessionProtocolError for internal encoding/encryption framing contract violations.

recv_json(*, timeout_s: float|None = None) -> dict
Preconditions:
- Session must be ACTIVE and have info/session keys established.
Behavior:
- Receives framed bytes from socket, using deframe_feed(deframe_state, chunk).
- Deframer is authoritative for resync + CRC validation + boundary detection.
- CRC-bad frames are discarded; receive continues until an OK frame is obtained or timeout.
- Decrypts schema-0 envelope with session_key_hex using decrypt_schema0_envelope().
- Parses decrypted payload as JSON.
Return:
- A Python dict representing the JSON object.
Validation:
- If decrypted JSON is not an object (dict), raises ValueError("expected JSON object").
Errors:
- SessionNotReadyError if not ACTIVE/ready.
- TimeoutError (or SessionIOError wrapping timeout) if no complete OK frame arrives before timeout.
- ValueError for invalid payload shape (short frame, invalid JSON, non-object JSON).
- SessionIOError for socket-level read errors / disconnect.
Side-effects:
- May invoke on_disconnected(err) if a disconnect is detected.
- Does NOT interpret message meaning; does NOT trigger auth/link/workflows.

pump_once(*, timeout_s: float = 0.0) -> dict | None
Purpose:
- “Event-driven” single-step pump: attempt to receive exactly one inbound JSON object and dispatch it.
Behavior:
- If timeout_s == 0.0: non-blocking-ish attempt (implementation uses socket timeout semantics).
- If a message is received:
  - calls on_message(msg) if provided
  - returns msg
- If no message is available before timeout:
  - returns None
Errors:
- Same as recv_json(), except timeouts may return None instead of raising if implemented that way.
- Any exception that indicates disconnect should cause close() and on_disconnected(err).

Errors (public exception surface)

SessionNotReadyError
- Raised when API methods requiring ACTIVE/ready state are used before connect() has successfully completed.
- Message should be clear English and actionable (e.g., “call connect() successfully first”).

SessionIOError
- Wraps OS/socket-level failures.
- Message should include endpoint context (host:port) where possible.

SessionProtocolError
- Raised when HELLO/session negotiation fails, framing invariants are broken, or decrypt/encrypt contract is violated.

ValueError
- Used for payload shape / JSON parsing problems (short frame, invalid JSON, wrong top-level type).

Threading and concurrency
- Session is not thread-safe unless explicitly documented otherwise.
- Do not call send_json/recv_json/connect/close concurrently from multiple threads/tasks without an external lock.
- If used with asyncio, use a single task to pump and serialize writes.

Contracts that dispatchers can rely on
- Once state is ACTIVE, recv_json() returns only JSON objects (dict) or raises.
- Session never initiates authentication or linking; it only transmits/receives schema-0 framed messages.
- on_message receives the raw JSON object exactly as parsed (no normalization), suitable for domain dispatchers.
- CRC resync and framing integrity are handled exclusively by the framing/deframer contract, not by dispatchers.

Non-goals (explicit)
- No automatic “keepalive” policy
- No retry/backoff loops
- No message correlation/workflow orchestration
- No privilege escalation/auth orchestration
- No linking/provisioning
