~kris/9p

llm9p

ref: bad7a7b752122022f1b03df6cc9e3deedf49a835 llm9p/internal/protocol/protocol.go -rw-r--r-- 8.0 KiB
bad7a7b7 — pdfinn feat(llmfs): async Write + per-session streaming for live token delivery 6 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
// Package protocol implements the 9P2000 protocol for the LLM filesystem.
//
// This is a minimal, clean implementation focused on the subset of 9P
// needed for LLM interaction. It is designed to be:
//   - Zero external dependencies (stdlib only)
//   - LLM-friendly (self-describing, good errors)
//   - Simple to understand and maintain
//
// The 9P protocol uses a simple request-response model over a bidirectional
// stream. Each message has a 4-byte size, 1-byte type, and 2-byte tag,
// followed by type-specific payload.
package protocol

import (
	"encoding/binary"
	"fmt"
	"io"
)

// Protocol constants
const (
	// Version is the protocol version we implement
	Version = "9P2000"

	// MaxMessageSize is the maximum size of a 9P message.
	// Must be large enough for system prompts (which can be 10-15KB with
	// tool documentation and reminders). Inferno's mount() proposes its
	// own msize; the negotiated size is min(client, server).
	MaxMessageSize = 65536

	// NoTag is used for Tversion/Rversion which don't use tags
	NoTag uint16 = 0xFFFF

	// NoFid represents an invalid fid
	NoFid uint32 = 0xFFFFFFFF
)

// Message types (T = request from client, R = response from server)
const (
	Tversion uint8 = 100
	Rversion uint8 = 101
	Tauth    uint8 = 102
	Rauth    uint8 = 103
	Tattach  uint8 = 104
	Rattach  uint8 = 105
	Terror   uint8 = 106 // never sent
	Rerror   uint8 = 107
	Tflush   uint8 = 108
	Rflush   uint8 = 109
	Twalk    uint8 = 110
	Rwalk    uint8 = 111
	Topen    uint8 = 112
	Ropen    uint8 = 113
	Tcreate  uint8 = 114
	Rcreate  uint8 = 115
	Tread    uint8 = 116
	Rread    uint8 = 117
	Twrite   uint8 = 118
	Rwrite   uint8 = 119
	Tclunk   uint8 = 120
	Rclunk   uint8 = 121
	Tremove  uint8 = 122
	Rremove  uint8 = 123
	Tstat    uint8 = 124
	Rstat    uint8 = 125
	Twstat   uint8 = 126
	Rwstat   uint8 = 127
)

// Open modes
const (
	OREAD  uint8 = 0  // open for read
	OWRITE uint8 = 1  // open for write
	ORDWR  uint8 = 2  // open for read/write
	OEXEC  uint8 = 3  // execute (unused in our context)
	OTRUNC uint8 = 16 // truncate file first
)

// File modes (high bits of Stat.Mode)
const (
	DMDIR    uint32 = 0x80000000 // directory
	DMAPPEND uint32 = 0x40000000 // append only
	DMEXCL   uint32 = 0x20000000 // exclusive use
	DMTMP    uint32 = 0x04000000 // temporary file
)

// Qid represents a unique file identifier
type Qid struct {
	Type    uint8  // QTDIR, QTFILE, etc.
	Version uint32 // version number for cache coherence
	Path    uint64 // unique path identifier
}

// Qid types
const (
	QTDIR    uint8 = 0x80 // directory
	QTAPPEND uint8 = 0x40 // append-only
	QTEXCL   uint8 = 0x20 // exclusive use
	QTTMP    uint8 = 0x04 // temporary
	QTFILE   uint8 = 0x00 // regular file
)

// Stat represents file metadata
type Stat struct {
	Size   uint16 // size of this stat structure (for wire format)
	Type   uint16 // server type
	Dev    uint32 // server device
	Qid    Qid    // unique id
	Mode   uint32 // permissions and flags
	Atime  uint32 // last access time
	Mtime  uint32 // last modification time
	Length uint64 // file length
	Name   string // file name
	Uid    string // owner
	Gid    string // group
	Muid   string // last modifier
}

// Encoder handles encoding messages to the wire format
type Encoder struct {
	w   io.Writer
	buf []byte
}

// NewEncoder creates a new encoder
func NewEncoder(w io.Writer) *Encoder {
	return &Encoder{
		w:   w,
		buf: make([]byte, MaxMessageSize),
	}
}

// Decoder handles decoding messages from the wire format
type Decoder struct {
	r   io.Reader
	buf []byte
}

// NewDecoder creates a new decoder
func NewDecoder(r io.Reader) *Decoder {
	return &Decoder{
		r:   r,
		buf: make([]byte, MaxMessageSize),
	}
}

// ReadMessage reads a complete 9P message from the stream
func (d *Decoder) ReadMessage() (msgType uint8, tag uint16, payload []byte, err error) {
	// Read 4-byte size
	if _, err := io.ReadFull(d.r, d.buf[:4]); err != nil {
		return 0, 0, nil, fmt.Errorf("reading size: %w", err)
	}
	size := binary.LittleEndian.Uint32(d.buf[:4])

	if size < 7 {
		return 0, 0, nil, fmt.Errorf("message too small: %d", size)
	}
	if size > MaxMessageSize {
		return 0, 0, nil, fmt.Errorf("message too large: %d", size)
	}

	// Read rest of message
	remaining := size - 4
	if _, err := io.ReadFull(d.r, d.buf[:remaining]); err != nil {
		return 0, 0, nil, fmt.Errorf("reading message: %w", err)
	}

	msgType = d.buf[0]
	tag = binary.LittleEndian.Uint16(d.buf[1:3])
	payload = d.buf[3:remaining]

	return msgType, tag, payload, nil
}

// WriteMessage writes a complete 9P message to the stream
func (e *Encoder) WriteMessage(msgType uint8, tag uint16, payload []byte) error {
	size := uint32(4 + 1 + 2 + len(payload))
	if size > MaxMessageSize {
		return fmt.Errorf("message too large: %d", size)
	}

	binary.LittleEndian.PutUint32(e.buf[0:4], size)
	e.buf[4] = msgType
	binary.LittleEndian.PutUint16(e.buf[5:7], tag)
	copy(e.buf[7:], payload)

	_, err := e.w.Write(e.buf[:size])
	return err
}

// String encoding helpers

func EncodeString(buf []byte, s string) int {
	binary.LittleEndian.PutUint16(buf[0:2], uint16(len(s)))
	copy(buf[2:], s)
	return 2 + len(s)
}

func DecodeString(buf []byte) (string, int) {
	if len(buf) < 2 {
		return "", 0
	}
	size := binary.LittleEndian.Uint16(buf[0:2])
	if len(buf) < int(2+size) {
		return "", 0
	}
	return string(buf[2 : 2+size]), int(2 + size)
}

// Qid encoding

func (q *Qid) Encode(buf []byte) int {
	buf[0] = q.Type
	binary.LittleEndian.PutUint32(buf[1:5], q.Version)
	binary.LittleEndian.PutUint64(buf[5:13], q.Path)
	return 13
}

func DecodeQid(buf []byte) (Qid, int) {
	if len(buf) < 13 {
		return Qid{}, 0
	}
	return Qid{
		Type:    buf[0],
		Version: binary.LittleEndian.Uint32(buf[1:5]),
		Path:    binary.LittleEndian.Uint64(buf[5:13]),
	}, 13
}

// Stat encoding

func (s *Stat) Encode(buf []byte) int {
	// Skip size field, we'll fill it at the end
	n := 2

	// Fixed fields
	binary.LittleEndian.PutUint16(buf[n:n+2], s.Type)
	n += 2
	binary.LittleEndian.PutUint32(buf[n:n+4], s.Dev)
	n += 4
	n += s.Qid.Encode(buf[n:])
	binary.LittleEndian.PutUint32(buf[n:n+4], s.Mode)
	n += 4
	binary.LittleEndian.PutUint32(buf[n:n+4], s.Atime)
	n += 4
	binary.LittleEndian.PutUint32(buf[n:n+4], s.Mtime)
	n += 4
	binary.LittleEndian.PutUint64(buf[n:n+8], s.Length)
	n += 8

	// Variable fields
	n += EncodeString(buf[n:], s.Name)
	n += EncodeString(buf[n:], s.Uid)
	n += EncodeString(buf[n:], s.Gid)
	n += EncodeString(buf[n:], s.Muid)

	// Fill in size (total - 2 for size field itself)
	s.Size = uint16(n - 2)
	binary.LittleEndian.PutUint16(buf[0:2], s.Size)

	return n
}

func DecodeStat(buf []byte) (Stat, int) {
	if len(buf) < 2 {
		return Stat{}, 0
	}

	s := Stat{}
	s.Size = binary.LittleEndian.Uint16(buf[0:2])

	if len(buf) < int(s.Size)+2 {
		return Stat{}, 0
	}

	n := 2
	s.Type = binary.LittleEndian.Uint16(buf[n : n+2])
	n += 2
	s.Dev = binary.LittleEndian.Uint32(buf[n : n+4])
	n += 4

	var qn int
	s.Qid, qn = DecodeQid(buf[n:])
	n += qn

	s.Mode = binary.LittleEndian.Uint32(buf[n : n+4])
	n += 4
	s.Atime = binary.LittleEndian.Uint32(buf[n : n+4])
	n += 4
	s.Mtime = binary.LittleEndian.Uint32(buf[n : n+4])
	n += 4
	s.Length = binary.LittleEndian.Uint64(buf[n : n+8])
	n += 8

	var sn int
	s.Name, sn = DecodeString(buf[n:])
	n += sn
	s.Uid, sn = DecodeString(buf[n:])
	n += sn
	s.Gid, sn = DecodeString(buf[n:])
	n += sn
	s.Muid, sn = DecodeString(buf[n:])
	n += sn

	return s, int(s.Size) + 2
}

// MessageName returns the human-readable name of a message type
func MessageName(t uint8) string {
	names := map[uint8]string{
		Tversion: "Tversion", Rversion: "Rversion",
		Tauth: "Tauth", Rauth: "Rauth",
		Tattach: "Tattach", Rattach: "Rattach",
		Rerror: "Rerror",
		Tflush: "Tflush", Rflush: "Rflush",
		Twalk: "Twalk", Rwalk: "Rwalk",
		Topen: "Topen", Ropen: "Ropen",
		Tcreate: "Tcreate", Rcreate: "Rcreate",
		Tread: "Tread", Rread: "Rread",
		Twrite: "Twrite", Rwrite: "Rwrite",
		Tclunk: "Tclunk", Rclunk: "Rclunk",
		Tremove: "Tremove", Rremove: "Rremove",
		Tstat: "Tstat", Rstat: "Rstat",
		Twstat: "Twstat", Rwstat: "Rwstat",
	}
	if name, ok := names[t]; ok {
		return name
	}
	return fmt.Sprintf("unknown(%d)", t)
}