Menu

lurpicUI Documentation

LDP — Lurpic Drive Protocol (major 2)

Last updated:

# LDP — Lurpic Drive Protocol (major 2)

LDP is the local automation seam: typed observations and GUI-backed actions between the lurpicUI runtime and external drivers over an authenticated Unix-domain socket. Protocol major 2 is a deliberate hard break: there are no v1 adapters, aliases, or compatibility dispatch. The advertised method set is the registry’s advertised surface — a major-2 build never puts an unported method on the wire. TestDocsMatchRegistry fails when this document and the registry drift.

# Enabling

LDP is off by default (no socket, no token, no listener, no goroutines). Enable per run:

LURPIC_DRIVE=on               # $XDG_RUNTIME_DIR/lurpic-<pid>/drive.sock, read grant
LURPIC_DRIVE=on:read,input    # explicit grants
LURPIC_DRIVE=unix:/abs/private/d.sock:read,input  # explicit absolute path

Unknown grants — including the v1 write spelling — fail startup.

# Sessions and requests

Each app launch creates a random instance identity. An authenticated session owns monotonically increasing request IDs (decimal strings on the wire), a bounded outcome ledger, and a connection generation. A session survives transport loss for ten minutes of wall time; resume proves the resume credential, increments the generation, and fences the replaced generation’s outstanding work — it never resumes execution automatically. Request IDs are admitted contiguously: a retried retained ID replays its original outcome, a changed payload is request.conflict, and an expired ID is request.expired — it can never execute again. There is no direct store mutation anywhere in the protocol: application actuation is GUI-backed input.* and cmd.execute only.

# Grants

A capability set baked in by whoever launches the app and immutable per session (resume never widens it): read, input, command, time, admin. command is the application-command grant; admin authorizes protocol administration only, never implicit input/command/time authority.

# Advertised methods (major 2)

The full table (including trusted in-process seam methods whose native v2 slices land in later slices) lives in drive/proto/registry.go. The advertised wire surface today:

  • sys.hello — version/instance/session negotiation; create with the launcher token, or resume with the instance/session/credential triple. The result carries instance/session/generation, next_id, grants, the protocol schema digest, frozen read-export contract digest, and limits document. The export digest covers declared names/field patterns, kinds, schemas, state classes, and byte bounds; live values and versions do not change it.
  • sys.ping — cut-exact liveness: the pong identifies the sealed cut and last submitted frame it observes.
  • cmd.list — lists visible GUI-backed commands with their exact positive contract versions.
  • cmd.execute — requires the command grant and an exact {name, version}. The runtime returns an accepted operation receipt, schedules the shared GUI command dispatcher at the next frame activation boundary, and sends op.completed only after the activation frame is ready and all command-owned job continuations have committed in a later ready frame. Their commits are fenced by the owning session generation. A disconnect cancels the operation and prevents any uncommitted continuation from mutating app state; already applied callback effects are not rolled back.
  • time.mode — requires the time grant. {"mode":"virtual"} acquires the session’s time lease and completes after its single zero-delta transition frame is ready. {"mode":"wall"} releases that lease after its transition frame. Repeating the current mode is idempotent and creates no frame. A driver transition during a held gesture is rejected.
  • time.advance — requires the time grant and the controlling time lease. {"duration_ns":"16666667"} advances engine time by the bounded integer duration and completes after exactly one frame has a ready surface. Reads while Virtual is parked create no frame. Pure jobs in the prior cut’s frontier finish before an authorized Virtual frame begins; their results commit in submission order during phase 1.
  • frame.run — requires time plus the union of grants used by its plan. It accepts GUI-backed cmd.execute steps and the registered input.* steps, an explicit bounded frame tail, and registered read-only then requests. Input macros expand into their canonical per-frame atoms. A plan-local bind_gesture on input.press and matching use_gesture on input.move/input.release retain capture only within that plan; every bound gesture must be released before success. Nested plans, mode changes, and operation-control requests are rejected. A plan completes only after its final submitted frame is ready.
  • op.status — reports the retained state and outcome of an operation owned by the authenticated session; expired outcomes return the operation.expired error.
  • op.cancel — requests cancellation of one of the authenticated session’s outstanding operations. Cancellation lands at the next safe boundary; already-applied effects stand and are never rolled back. Cancelling an already-terminal operation is idempotent and returns its outcome. The terminal op.completed notification still arrives for a cancelled operation.
  • session.state — reports the authenticated session’s own protocol state: generation, next request ID, earliest retained outcome, active operation IDs, recovery-expiry metadata while detached, and the current time ownership. It never reports a credential.
  • session.list — requires the admin grant. Returns the redacted session metadata page (ID, lifecycle state, generation, grants), sorted by session ID, with 1..32 page bounds and a next-cursor continuation.
  • session.revoke — requires the admin grant. Terminally revokes one session with a typed reason (operator, policy, or instance.shutdown), fences its outstanding operations at the next safe boundary, and cancels its subscriptions. Revoking an unknown or already-revoked session returns the session.expired error.
  • own.state — reports the immutable control view: who owns input (human or client, with session/generation attribution), held-gesture count, the engine clock mode, and the time-ownership lease metadata.
  • sub.watch — requires the read grant. Creates a subscription over the shared bounded event log: exactly one of after (a decimal cursor) or snapshot:true (the current export-version snapshot plus the stream base), a credit window 1..8, and a filter selecting event classes (state.changed, tree.changed, frame.completed, frame.ready, own.preempted, operation.terminal) and, for state.changed, export names. A position below the retained floor returns stream.invalid_cursor rather than pretending the gap is deliverable.
  • sub.state — reports a stream’s lifecycle (live/expired/cancelled), terminal reason, sent/acknowledged cursors, credit, and the current retention window (floor/head) for the owning session.
  • sub.cancel — terminally cancels a stream (idempotent); repeated calls return the terminal state.
  • Server notifications are op.completed, sub.data (one sealed record per notification, carrying the events that matched the filter), sub.expired (once per terminal stream), own.preempted (retained event; authoritative state remains queryable), and sys.shutdown on server stop. Client notifications are sys.keepalive (the lease clock: a controller whose session owns input or time and stays silent beyond the documented keepalive idle bound loses that lease at the next safe boundary) and sub.ack (acknowledges a delivered cursor and returns credit). Client notifications carry no ID and never consume one.
  • locate.resolve — resolves one exact author ID, role/name, scoped key, accessible name, kind, or hit-map coordinate locator. Ambiguity candidates are typed error details, capped at ten with an explicit truncation flag.
  • tree.dump — returns pre-order structural pages from a bounded cut observation index. The first page captures an immutable, session-owned snapshot; later pages use its opaque snapshot ID and decimal-string offset. Snapshots expire after 60 seconds and count toward an 8 MiB per-runtime retention budget. Index capacity exhaustion returns the resource-limit error and leaves ordinary app rendering available.
  • facet.query — reports structural/layout/focus/hit/layer facts and only explicitly declared typed detail captures.
  • store.list — pages the frozen explicit export catalog, including each export’s domain/view class, kind, schema name, version, and byte bound.
  • store.read — reads a named scalar or bounded collection page, or an explicitly declared {target, field} capability. A collection whose first page is incomplete returns a session-owned snapshot handle; later pages use that handle and a decimal-string offset, preserving one source version for the full read. Handles expire after 60 seconds and count toward the 8 MiB per-runtime frozen-export budget. Undeclared fields fail closed; derived/detail exports use their dedicated read method.
  • derived.read — returns the last computed derived value and dirty/version metadata through Peek without flushing, evaluating, or changing counters.
  • input.click, input.press, input.release, input.move, input.drag, input.type, input.scroll, and input.focus — require the input grant and enter the ordinary platform event queue. Each synthesized atom is collected at a frame boundary; Virtual mode remains parked until the time owner explicitly advances it. Device defaults to the current input modality (mouse unless touch is active) and accepts explicit mouse or touch. Mouse click is move/press/release; touch click is touch-down/touch-up. Drag uses from and to location oneofs plus move_count (default 5, range 1..128), then emits its canonical start atom, interpolated moves, and release. Point methods accept window:"primary"; unavailable windows fail explicitly. Results report the selected device and one atom per frame. input.press returns a session- and connection-generation-bound gesture_handle; matching input.move and input.release requests must present it. Human input or connection loss cancels the unstarted remainder and clears capture at the next safe boundary. input.focus performs real Tab/Shift-Tab traversal and reports the number of hops.

Streams and artifact transfer remain later slices. The advertised registry is the authoritative list of methods implemented by this build. Read exports are declarations made by app/mark authors; the framework does not reflect over Go fields or infer writable capabilities.

# Wire

UTF-8 NDJSON, one object per line, 4 MiB hard bound. Identifiers that carry uint64 precision (request IDs, cuts, frames) are decimal strings. Decoding is strict: duplicate JSON keys, unknown envelope fields, wrong major, non-canonical IDs, non-object params, nesting beyond 32, non-finite/overflowing numbers, and trailing data are rejected before admission with typed errors (protocol.malformed, protocol.line_limit, protocol.version_mismatch) that never consume a request ID.

# Security

The socket is the trust boundary: a private (0700), UID-owned parent directory; socket and token inodes 0600; exclusive no-follow token creation; a pre-existing socket or token path is a startup failure (stale paths are cleaned by an operator, never unlinked blind); token comparison is constant-time. Android: LDP compiles only in debuggable builds; the socket never leaves the device (adb forward for remote debugging); MCP exposure is the bridge author’s decision (see lurpic mcp init).

# Ops runbook

  • Command automation: configure the launcher with read,command grants, then use lurpic drive commands, lurpic drive execute --name <name> --version <n>, and lurpic drive op-status --operation-id <id>. Add --session-file <path> to reuse a local 0600 credential file across CLI invocations. Without that option each invocation creates a new session.
  • Observing a running app: lurpic drive watch --snapshot --duration 30s subscribes to the shared event log (filter with --classes state.changed,frame.completed, --names e1.chartType, --credit 1..8) and prints every delivered record with its cut/frame attribution, acknowledging each cursor. lurpic drive own-state reports who owns input and time; lurpic drive session-state reports this session’s ledger state; lurpic drive op-cancel --operation-id <id> cancels an outstanding operation at the next safe boundary. With the admin grant, lurpic drive session-list pages the redacted session table and lurpic drive session-revoke --session-id <id> --reason operator terminally revokes one.
  • Token rotation: restart the app. The token file is regenerated next to the socket on every launch; there is no rotation without restart.
  • Lost command completion: execute resumes the session and retries the same request ID after a lost receipt, then queries op.status if the completion notification is lost. Pass --session-file to retain the 0600 credential if the CLI process exits; reconnect with that file and query the known operation ID. Do not issue the command under a new ID until its old outcome is known. A new app instance is a new identity and old outcomes are unavailable (instance.changed).
  • Virtual time: request time.mode with mode=virtual before advancing; only the owning session may submit time.advance or frame.run. Disconnect releases ownership independently of engine time. Use time.advance with a duration_ns value from 1 through 1000000000 to request one frame, or frame.run for a bounded, ordered command plan. An input-only client never advances frames implicitly.
  • Request/result expiry: retention is bounded (entries and bytes per session); an evicted ID answers request.expired and the high-water mark never moves backwards.
  • Preemption expectations: any human (platform) event severs an in-flight input.* step mid-gesture; ownership flips to human and the verb replies input.preempted. Drivers must handle it rather than retry blindly. Disconnect fences a lost generation’s future work at the next safe boundary; applied effects stand.
  • Budget interpretation: drive commands admit at the frame cut on the runtime thread; a frame carrying commands pays their handler cost inside that frame’s budget. The E5 drive row shows per-frame drained counts.
  • Android: the drive seam compiles only into debuggable builds (the lurpicdrive build tag, set by lurpic build android for debug APKs). Release builds refuse LURPIC_DRIVE at startup with “drive unavailable”. Forward the socket with adb forward for remote lurpic drive use.
On this page