# 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:

```text
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:

```text
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:

```text
🪢: 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:

```text
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:

```text
🧩: <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:

- The `#` MUST be the first byte of the line.
- The entire line including the `#` and the terminating LF MUST NOT exceed 128
  bytes.
- Comments are stripped before any hash computation. They are not part of
  canonical fact blocks, advertisement records, lacegram hashes, or exchange-plan hashes.
- Inline comments are not supported. A `#` that is not the first byte of a line
  is not a comment marker.
- Comment lines MAY appear anywhere a fact line, resource block, record, or
  phase separator is expected. They MUST NOT appear inside record bytes or
  resource bodies.
- Consecutive comment lines are not allowed. Two `#`-prefixed lines without an
  intervening fact line, resource block, record markline, or blank-line phase
  separator MUST be rejected.

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

Example:

```text
# 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.

```text
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:

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

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

```text
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:

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

Require encrypted transport:

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

Require both transport proof and membership:

```text
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.
