Lace / spec

Lace-060 ยท Inter-Lace Transfer Protocol (ILTP)

Tags: transport, iltp

Purpose

ILTP makes 050 interlace concrete over byte streams. It enables interlace machines to run over stdio, Unix sockets, TCP, or WebSocket streams by carrying setup facts, control facts, inline canonical resource text, and self-delimiting record bytes, while exposing transport identity and encryption claims only as explicit runtime facts.

This document is for ILTP binding and transport implementors. After reading it, an implementor should be able to parse transport addresses, carry interlace fact blocks, resource text, and record bytes over a stream, make required module/plan bytes available, and expose transport proofs as runtime facts.

Defines

Address strings, stream format, knot preface, fact blocks, puzzle-piece resource text blocks, record bytes, phase separation, transport bindings, peer identity proof, transport encryption indication, and limits.

Address strings

An address string identifies a transport endpoint. Most addresses use:

scheme:address

stdio is also valid as a special literal with no colon.

Scheme Address form Default port
stdio (empty) โ€”
unix /absolute/path โ€”
tcp host:port 4790
ws host[:port][/interlace] 80
wss host[:port][/interlace] 443

Host is a hostname, IPv4 address, or bracketed IPv6 address. Port is a decimal integer; when omitted, the scheme default applies. For ws and wss, an absent path means /interlace. Other paths are binding-defined deployment details, not part of the standard address form. Query strings are not part of the standard address form. URL-style ws:// and wss:// strings are not standard Lace address strings.

Examples:

stdio
unix:/tmp/lace.sock
tcp:127.0.0.1:4790
tcp:example.com:9000
tcp:[::1]:4790
ws:127.0.0.1:4790/interlace
wss:example.com/interlace

Stream format

The ILTP stream format is self-delimiting: a receiver can distinguish fact lines, resource blocks, stored records, comments, and phase separators from leading bytes, then use the relevant line, resource, or 010 record rules to know where each item ends. This keeps ILTP bindings simple without adding binary framing headers.

An ILTP stream is a bidirectional byte stream carrying lines of UTF-8 text, inline canonical resource text, and raw stored record bytes. No binary framing headers are added; the content is self-describing.

Session preface

Each stream direction begins with the knot preface line:

๐Ÿชข: iltp/1

The first bytes of an ILTP stream direction MUST be ๐Ÿชข: (0xF0 0x9F 0xAA 0xA2 0x3A 0x20). The preface line MUST end with LF; CR and CRLF are invalid. It is ILTP-level framing, not a 050 fact block, not a Datalog fact, and not followed by a blank-line phase separator. A receiver MUST reject a stream direction whose first line is not the exact knot preface line, or whose next stream item after the preface is a blank-line phase separator.

Fact lines

Fact lines follow the 050 fact block syntax:

Predicate('arg',...)

Lines end with LF only. CR and CRLF are invalid. Every fact line begins with an ASCII letter [A-Za-z].

Resource text blocks

A resource text block supplies canonical text bytes for an identifier referenced by setup or hello facts. It begins with a puzzle-piece marker line:

๐Ÿงฉ: <id> <kind>
<canonical text line>
...

The marker line syntax is exactly ๐Ÿงฉ: <id> <kind> followed by LF: one ๐Ÿงฉ: marker (0xF0 0x9F 0xA7 0xA9 0x3A 0x20), <id>, one ASCII space, <kind>, then LF. CR, CRLF, trailing spaces, and extra tokens are invalid. <id> and <kind> are UTF-8 NFC text tokens with no spaces or tabs. The standard kinds are:

Kind Id shape Body bytes
lacegram R.<b64a> canonical 030 lacegram text
exchange-plan E.<b64a> canonical 040 exchange-plan transcript

The body is one or more non-empty UTF-8 NFC text lines ending with LF. A blank line ends the resource block. The blank-line terminator is not part of the resource. The recovered resource bytes are the body lines joined by LF with no trailing LF. Resource bodies MUST NOT contain CR, CRLF, or blank lines. Resource body lines MUST NOT be comment lines or begin with record, resource, or session markers.

Resource blocks are ILTP-level framing. They are not Datalog runtime facts, advertisement facts, setup facts, hello facts, peer advertisement facts, records, or 050 phases. A resource block MAY appear anywhere a new fact block or record batch item could begin, before the referenced object is needed. If a standard stream sender has not explicitly established that the peer already has a referenced R.* or E.* object, it MUST send the corresponding resource before the first fact that references it.

After reading a resource block, the receiver MUST validate that the recovered bytes are already canonical and recompute the identifier. It MUST NOT accept non-canonical bytes by canonicalizing them after receipt. A lacegram resource must validate as canonical 030 lacegram text and recompute to the announced R.<b64a>; source containing = as a body atom or removed builtins such as Prefix is invalid in resource text. An exchange-plan resource must validate as a canonical 040 exchange-plan transcript and recompute to the announced E.<b64a>. An exchange-plan resource supplies only the plan transcript; referenced operand R.* bytes must be available separately.

Duplicate identical resource blocks are allowed. A conflicting resource body for an already known identifier, malformed marker line, unknown kind, id/kind shape mismatch, non-canonical body, identifier mismatch, oversized resource, or resource count overflow is a stream error.

Record bytes

A stored record begins with the markline ๐Ÿ–ง: (six bytes: 0xF0 0x9F 0x96 0xA7 0x3A 0x20). After the markline, the receiver parses one complete stored record as defined by 010. For a Blob record this includes reading exactly Data-Length bytes of data; for a Plex record or Seal record this includes parsing the full embedded record chain. During a record transfer response phase, a stored record item MUST match a current outstanding 050 requested hash; otherwise it is a malformed or out-of-phase record response and the current round aborts.

At the start of a stream item, a receiver dispatches by leading bytes:

Leading bytes Meaning
๐Ÿชข: Session preface, only as the first line in each stream direction
[A-Za-z] Fact line
๐Ÿงฉ: Resource text block
๐Ÿ–ง: Stored record markline
โ‹ฏ๐Ÿ–ง: Trailer-hash record open marker, only when the binding supports that extension
# Comment line
LF Blank-line phase separator

Parsers MUST match the full marker byte sequence for ๐Ÿชข:, ๐Ÿงฉ:, ๐Ÿ–ง:, and โ‹ฏ๐Ÿ–ง:; matching only the first byte is not sufficient because several markers begin with 0xF0. Any other leading byte sequence where a stream item is expected is invalid.

The standard ILTP binding carries stored record bytes. A WebSocket binding is a byte-stream adapter like TCP after the HTTP upgrade. It MUST carry ILTP stream bytes in binary WebSocket message payloads and MUST NOT use WebSocket text messages as protocol facts or fact blocks. WebSocket frame and message boundaries are transport fragmentation only and MUST NOT be treated as fact-block, phase, record, resource, or interlace-message boundaries.

A binding MAY also support 010 trailer-hash record transport as an extension. In that case a record may begin with the trailer open marker โ‹ฏ๐Ÿ–ง: whose first byte is 0xE2, and the binding MUST parse and restore it according to 010 before validation.

Comment lines

Comments make captured streams inspectable and self-documenting without entering fact blocks, record bytes, or hash computations. A line whose first byte is # (0x23) is a comment. Comments are ignored at the stream level and MUST NOT be interpreted as fact lines or record data.

Rules:

A conforming parser MUST skip valid comment lines silently and MUST reject malformed comment placement such as consecutive comment lines.

Example:

# Round 1: Alice advertises
Advertised('P.a.H3','V.alice.H3')
AdvertisedField('P.a.H3','V.alice.H3','Group','0','u')

# Bob requests the new record
MayRequest('P.a.H3')

Phase separation

A blank line (bare LF, immediately after a fact line LF or valid comment line LF) ends the current fact block. The next non-blank content begins the next phase.

Interlace control fact blocks are each terminated by a blank line. They may carry setup, hello, advertisement, reconciliation, request, availability, or tick facts. The interlace engine drives the phase sequence; ILTP does not label phases. A blank line with no preceding fact lines in the current block represents an empty block. A block containing only comments is also empty after comments are stripped.

Module and exchange-plan byte availability

ExchangeOperand(Index,R,Origin,Facet) and HelloExchangePlan(E) facts identify canonical module or exchange-plan bytes. An exchange plan references canonical operand and plan bytes. The ILTP binding MUST make canonical bytes for unknown announced identifiers available to the peer before the peer needs to validate, compile, lower, or evaluate them.

The standard interoperable ILTP mechanism is the ๐Ÿงฉ: resource text block. A binding MAY also satisfy byte availability from a cache, startup configuration, or another pre-established source. If a sender has not established that the peer already has the needed canonical bytes, it sends the corresponding resource text block before the first ExchangeOperand, HelloExchangePlan, or plan transcript that depends on those bytes. If required canonical bytes are still unavailable when needed, setup or negotiation fails.

Bidirectional streaming

Both directions are independent. A peer MAY send fact lines or records while simultaneously receiving from the other side. The interlace engine reassembles logical phases from the received content.

End of stream

A blank line terminates the current phase: it ends a fact block after a sequence of fact lines, or the record transfer batch after a sequence of record deliveries. Record data itself is parsed by 010 length or trailer rules; the blank line is read only after a complete record has ended. When a peer has no more content to send in the current phase, it sends a blank line. The other side detects the end of a fact block or record batch by the blank line.

Transport disconnect ends the stream in both directions. A disconnect at a phase boundary is a clean close with the facts and records received up to that point. A disconnect in the middle of a fact line, comment line, resource block, or record is an unexpected abort for the current interlace round.

Example stream trace

Identifiers in this trace are symbolic.

A โ†’ B: ๐Ÿชข: iltp/1\n
B โ†’ A: ๐Ÿชข: iltp/1\n

A โ†’ B: ๐Ÿงฉ: R.a lacegram\nSelectHave(P) :- Have(P).\nSelectAdvertised(P,S) :- Advertised(P,S).\n\nExchangeOperand('0','R.a','V.alice.H3','selector')\n\n
B โ†’ A: ๐Ÿงฉ: R.b lacegram\nSelectHave(P) :- Have(P).\nSelectAdvertised(P,S) :- Advertised(P,S).\n\nExchangeOperand('1','R.b','V.bob.H3','selector')\n\n

# Both sides validate module bytes, compile exchange plan E.final.

A โ†’ B: HelloExchangePlan('E.final')\nHelloTAI('1640995200:000000000')\nHelloTickInterval('10000000000')\nHelloRecordFormat('H3')\nHelloAdvertisedField('Type')\n\n
B โ†’ A: HelloExchangePlan('E.final')\nHelloTAI('1640995200:000000000')\nHelloTickInterval('10000000000')\nHelloRecordFormat('H3')\nHelloAdvertisedField('Type')\n\n

A โ†’ B: Advertised('B.a.H3','V.alice.H3')\nAdvertisedField('B.a.H3','V.alice.H3','Type','0','B')\n\n
B โ†’ A: Advertised('B.b.H3','V.bob.H3')\nAdvertisedField('B.b.H3','V.bob.H3','Type','0','B')\n\n

A โ†’ B: MayRequest('B.b.H3')\n\n
B โ†’ A: MayRequest('B.a.H3')\n\n

A โ†’ B: ๐Ÿ–ง: B.a.H3\nData-Length: 11\n\nhello world\n\n
B โ†’ A: ๐Ÿ–ง: B.b.H3\nData-Length: 5\n\ndata!\n\n

Transport and runtime facts

050 defines the standard exchange runtime facts:

Predicate Arity Meaning
Here(V) 1 local verifier for this context, if any
Peer(V) 1 peer verifier proven by the transport or binding
Transport(S) 1 address string of the active connection
TransportEncrypted() 0 active transport provides confidentiality, if true
StartTAI(T) 1 shared negotiated exchange-start TAI
TickTAI(T) 1 shared tick time for this evaluation
ClockSkewSeconds(N) 1 absolute clock skew in decimal seconds

This spec defines how transport bindings may justify Peer, Transport, and TransportEncrypted. Time facts are negotiated as defined by 050.

Transport(S) is set to the address string for the active connection.

TransportEncrypted() is set only when the transport binding proves that stream bytes are encrypted for confidentiality between the endpoints. When absent, transport encryption is not proven to the rule engine.

Peer(V) is set only when the transport or application-defined binding proves that the peer controls verifier V. When absent, peer verifier identity is not proven to the rule engine.

Before evaluation begins, the runtime sets exchange runtime facts from the transport or application-defined binding. HelloSigner(V) is valid only when the binding proves that the sender controls verifier V; it is not self-asserted. When a remote HelloSigner(V) is accepted, the runtime sets Peer(V). A received HelloSigner without valid proof is a negotiation failure.

When Peer(V) is absent, no remote verifier identity is available to rules. Lacegrams MAY use Peer(V) to gate authority on proven identity; rules that depend on Peer(V) produce no results when the peer identity is unproven.

When TransportEncrypted() is present, the runtime asserts only transport confidentiality. It does not prove peer identity. When it is absent, lacegrams that require TransportEncrypted() produce no results.

Implementations MUST NOT expose private implementation state as runtime facts to peer-origin modules unless a profile explicitly defines that exposure.

Per-transport rules

stdio

The caller controls both stdin and stdout of the child process. The runtime MAY set Peer(V) when the caller or parent process proves the peer verifier. Transport('stdio') is set. TransportEncrypted() is absent unless an application-defined binding proves encryption.

unix

The runtime MAY use OS peer credentials, such as SO_PEERCRED on Linux or getpeereid on macOS, to set Peer(V). The mapping from OS identity to verifier is deployment-defined. When no mapping exists, Peer(V) is absent. Transport('unix:/absolute/path') is set to the bound path. TransportEncrypted() is absent unless an application-defined binding proves encryption.

tcp

Bare TCP provides no peer identity proof and no encryption proof. Peer(V), HelloSigner(V), and TransportEncrypted() are absent unless an application-defined binding proves them. Transport('tcp:<host>:<port>') is set to the remote endpoint address for the active connection, using the explicit or default port chosen by address parsing.

ws and wss

A WebSocket transport carries the ILTP stream after WebSocket upgrade. The upgrade request is connection setup only. After upgrade, the binding MUST drive interlace over the WebSocket stream and MUST NOT define application commands such as GET note, POST note, or remote coordinate listing as protocol operations.

Transport('ws:...') or Transport('wss:...') is set to the accepted WebSocket address, with an omitted path represented as /interlace. wss MAY set TransportEncrypted() when TLS validation proves stream confidentiality for the active connection. Bare ws does not prove encryption. Neither ws nor wss proves peer verifier identity by itself; Peer(V) and HelloSigner(V) are present only when the WebSocket binding or an application-defined proof verifies control of V.

Trusted prepared-policy examples

Accept only from a transport-proven peer:

MayRequest(P) :- AdvertisedField(P,S,'Signed-By',_,V), Peer(V).

Accept from a peer whose Seal records validate as a known member:

MayRequest(P) :- AdvertisedField(P,S,'Signed-By',_,V), Member(V).
Member(V) :- Have(P), Field(P,'Group',_,'u'), Field(P,'App',_,'member'), Field(P,'Name',_,V).

Only accept over a local unix socket:

MayRequest(P) :- AdvertisedField(P,S,'Signed-By',_,V), Transport(T), TextShape(T,'unix:','','').

Require encrypted transport:

MayRequest(P) :- AdvertisedField(P,S,'Signed-By',_,V), TransportEncrypted().

Require both transport proof and membership:

MayRequest(P) :- AdvertisedField(P,S,'Signed-By',_,V), Peer(V), Member(V).

These examples hand-author MayRequest(P) and therefore are trusted prepared-policy examples. In the exchange-plan route, ordinary authoring uses 040 modules with SelectHave and SelectAdvertised; setup compiles them into generated MaySend and MayRequest enforcement predicates rather than sending ad hoc wire commands.

Limits

Limit Value
Max fact line length 1024 bytes excluding LF
Max comment line length 128 bytes including LF
Max resource marker line length 1024 bytes excluding LF
Max resource text bytes 1 MiB per resource
Max resource text lines 4096 per resource
Max resource count 256 per exchange

Record limits follow 010. Fact block and interlace phase limits follow 050.