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.
| Name | Type | Description |
|---|---|---|
vrequired | integer | Canonical unsigned decimal integer 1; fractions and exponent notation are invalid. |
id | ASCII string | Required on present, answer, and resolved; optional on error; absent on status. When present, 1–64 characters matching [A-Za-z0-9._:-]+. |
enumsrequired | lowercase | Case-sensitive lowercase values only. |
unknown fields | bounded JSON | Valid, 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 closed | A 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.
| Name | Type | Description |
|---|---|---|
protocol / specrequired | string | nexting-device plus a non-empty spec version; the official App applies the negotiated bounds. |
wire / profilesrequired | arrays | wire includes 1; profiles includes approval/1. |
model / fwrequired | string | Each is non-empty printable and at most 64 UTF-8 bytes. |
max_message_bytesrequired | integer | At least 512; official implementations cap at 4096. |
max_summary_bytesrequired | integer | The 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.
| Name | Type | Description |
|---|---|---|
vrequired | integer | Canonical wire major 1. |
trequired | string | Exactly present. |
idrequired | string | Current request ID. |
sumrequired | string | 0–240 UTF-8 bytes. |
optrequired | tuple | Exactly ["allow","deny"] in that order. |
ttlrequired | integer | Relative 1–300000 ms from receipt. |
{"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.
| Name | Type | Description |
|---|---|---|
vrequired | integer | Canonical wire major 1. |
trequired | string | Exactly answer. |
idrequired | string | Must match the visible request. |
chrequired | enum | allow or deny. |
{"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.
| Name | Type | Description |
|---|---|---|
vrequired | integer | Canonical wire major 1. |
trequired | string | Exactly resolved. |
idrequired | string | Request being finalized. |
rrequired | enum | answered, expired, cancelled, or replaced. |
{"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.
| Name | Type | Description |
|---|---|---|
vrequired | integer | Canonical wire major 1. |
trequired | string | Exactly error. |
id | string? | Optional related request ID. |
coderequired | enum | Required protocol error code. |
{"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.
| Name | Type | Description |
|---|---|---|
vrequired | integer | Canonical wire major 1. |
trequired | string | Exactly status. |
agentsrequired | array | 0–8 entries; empty clears all slots. |
slotrequired | integer | Unique 0–7 within the frame. |
staterequired | enum | Required status state. |
label | string? | Optional 1–64 UTF-8 bytes, with no control characters. |
{"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.
| Name | Type | Description |
|---|---|---|
navigation/1 | both | nav_present, nav_move, nav_select, nav_resolved |
keys/1 | both | keymap and replay-protected key_event |
rotary/1 | both | rotary_map, rotary_event, rotary_press |
voice/1 | both | voice_event and voice_state; control only, no audio or transcript |
text/1 | down | bounded plain text for one declared display channel |
usage/1 | down | usage and usage_clear for informational display counters |
config/1 | both | atomic versioned config plus config_result |
{"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.
node --test reference/js/test/*.test.mjsprotocol/vectors/ as canonical behavior, not illustrative samples.