Build

Wire protocol

Wire major 1 · 9 negotiated profiles

Wire major 1 is a small fail-closed contract: nine negotiated profiles, 20 compact JSON message variants, one LF delimiter, bounded fields, and no internal Agent or account identifiers.

1. Wire rules

Every logical message is one compact UTF-8 JSON object followed by exactly one LF byte. The complete frame is at most 4096 bytes including that LF. Framing may split across transport chunks, but receivers perform no partial execution: they validate the complete object before changing state. A raw decode API gets delimiter-stripped JSON from its line decoder. A receiver does not accept interleaved fragments from different logical messages. It clears current framing state after invalid UTF-8, oversize input, or disconnect.

All nine negotiated profiles use this same envelope; Device Info gates each optional profile before traffic begins.

NameTypeDescription
vrequired
integerCanonical unsigned decimal integer 1; fractions and exponent notation are invalid.
id
ASCII stringRequired on present, answer, and resolved; optional on error; absent on status. When present, 1–64 characters matching [A-Za-z0-9._:-]+.
enumsrequired
lowercaseCase-sensitive lowercase values only.
unknown fields
bounded JSONValid, bounded unknown optional fields are ignored. Their values allow at most eight nested levels; names have no independent length limit inside the frame ceiling.
invalid inputrequired
fail closedA conforming decoder rejects bad UTF-8 and duplicate known keys at every nesting level, including equivalent JSON escapes; it also rejects unknown types, missing fields, and bad bounds.

Device Info contract

Device Info is a compact UTF-8 JSON value read from its GATT characteristic; it is not newline-framed. All eight base fields below are required: wire must include 1, and profiles must include approval/1.model and fw are each non-empty printable strings of at most 64 UTF-8 bytes. Unknown capability fields are ignored. Status capability requires both status/1 in profiles andstatusSlots >= 1. Missing or zerostatusSlots means no status traffic.

NameTypeDescription
protocol / specrequired
stringnexting-device plus a non-empty spec version; the official App applies the negotiated bounds.
wire / profilesrequired
arrayswire includes 1; profiles includes approval/1.
model / fwrequired
stringEach is non-empty printable and at most 64 UTF-8 bytes.
max_message_bytesrequired
integerAt least 512; official implementations cap at 4096.
max_summary_bytesrequired
integerThe official App accepts the bounded value negotiated from Device Info.
statusSlots
integer?Optional 0–8 capability; positive only with status/1.

2. present

The host presents one eligible approval and starts a relative monotonic lifetime. A new present replaces the current request; the host resolves the old one as replaced before presenting the new one.

host → devicepresentHost presents one approval
NameTypeDescription
vrequired
integerCanonical wire major 1.
trequired
stringExactly present.
idrequired
stringCurrent request ID.
sumrequired
string0–240 UTF-8 bytes.
optrequired
tupleExactly ["allow","deny"] in that order.
ttlrequired
integerRelative 1–300000 ms from receipt.
canonical framejson
{"v":1,"t":"present","id":"r1","sum":"Allow git push?","opt":["allow","deny"],"ttl":30000}

Device state becomes Pending. Prompts with a third option, persistent grant, multi-select behavior, or ambiguous option meaning stay on the phone.

3. answer

The device answers only its visible current request. It then enters Waiting Resolution and may resend the identical answer after 1000 ms until resolution or local TTL expiry; it never changes the choice.

device → hostanswerDevice reports one physical choice
NameTypeDescription
vrequired
integerCanonical wire major 1.
trequired
stringExactly answer.
idrequired
stringMust match the visible request.
chrequired
enumallow or deny.
canonical framejson
{"v":1,"t":"answer","id":"r1","ch":"allow"}

The host deduplicates by request ID, locks the first hardware choice, and allows at most one authoritative action-sink attempt in flight.

4. resolved

A matching resolved clears visible approval UI and cached retry state immediately, whether the device is Pending or Waiting Resolution. The reason is answered when the authoritative sink accepts, expired when the lifetime ends, cancelled when the host withdraws, or replaced when a newer request supersedes it.

host → deviceresolvedHost finalizes one request
NameTypeDescription
vrequired
integerCanonical wire major 1.
trequired
stringExactly resolved.
idrequired
stringRequest being finalized.
rrequired
enumanswered, expired, cancelled, or replaced.
canonical framejson
{"v":1,"t":"resolved","id":"r1","r":"replaced"}

A late or unknown resolution must not clear a different current request.

5. error

Error reports are diagnostic and fail closed. The error id is optional; code is required and is one of bad_message, message_too_large, unsupported_version, unsupported_profile, unknown_request, not_authorized, or busy. Implementations may silently drop malformed or attacker-controlled input when a reply would amplify traffic or reveal authorization state.

host ↔ deviceerrorEither endpoint reports a bounded protocol failure
NameTypeDescription
vrequired
integerCanonical wire major 1.
trequired
stringExactly error.
id
string?Optional related request ID.
coderequired
enumRequired protocol error code.
canonical framejson
{"v":1,"t":"error","id":"r1","code":"unknown_request"}

Receiving an error does not imply partial execution of the rejected message.

6. status

Status is a volatile full replacement. Each valid frame atomically replaces every tracked slot; agents: [] clears all. Agent state is idle, thinking, working, complete, needs_input, or error. A malformed frame preserves the previous state, and disconnect or reboot clears rendered status.

host → devicestatusHost replaces all Agent indicators
NameTypeDescription
vrequired
integerCanonical wire major 1.
trequired
stringExactly status.
agentsrequired
array0–8 entries; empty clears all slots.
slotrequired
integerUnique 0–7 within the frame.
staterequired
enumRequired status state.
label
string?Optional 1–64 UTF-8 bytes, with no control characters.
canonical framejson
{"v":1,"t":"status","agents":[{"slot":0,"state":"thinking","label":"fix login bug"}]}

A device renders the lowest numbered physical slots it supports and ignores higher ones. Status never creates, answers, expires, or resolves an approval.

Interaction profiles

These frozen profiles share the same framing and fail-closed rules. Device Info must declare the exact profile before the Host sends or accepts its messages.

NameTypeDescription
navigation/1
bothnav_present, nav_move, nav_select, nav_resolved
keys/1
bothkeymap and replay-protected key_event
rotary/1
bothrotary_map, rotary_event, rotary_press
voice/1
bothvoice_event and voice_state; control only, no audio or transcript
text/1
downbounded plain text for one declared display channel
usage/1
downusage and usage_clear for informational display counters
config/1
bothatomic versioned config plus config_result
physical input examplesjson
{"v":1,"t":"key_event","slot":3,"event":"press","seq":18}
{"v":1,"t":"rotary_event","slot":0,"delta":1,"seq":19}
{"v":1,"t":"voice_event","event":"start","seq":20}

7. Test vectors

Run the executable JavaScript reference tests from the exported source root. They load all nine profile vectors and exercise codec, framing, relay races, sequence rules, and rejection cases.

source rootsh
node --test reference/js/test/*.test.mjs
Treat every file under protocol/vectors/ as canonical behavior, not illustrative samples.