banger/internal/sessionstream/sessionstream.go
Thales Maciel 37c4c091ec
Add guest sessions and agent VM defaults
Add daemon-backed workspace and guest-session primitives so host
orchestrators can prepare /root/repo, launch long-lived guest commands,
and attach to pipe-mode sessions over the local stdio mux bridge.

Persist richer session metadata and launch diagnostics, preflight guest
cwd/command requirements, make pipe-mode attach rehydratable from guest
state after daemon restart, and allow submodules when workspace prepare
runs in full_copy mode.

At the same time, stop vm run from auto-attaching opencode, make it
print next-step commands instead, and make glibc guest images more
agent-ready by installing node, opencode, claude, and pi while syncing
opencode/claude/pi auth files into work disks on VM start.

Validation:
- GOCACHE=/tmp/banger-gocache go test ./...
- make build
- banger vm workspace prepare --help
- banger vm session --help
- banger vm session start --help
- banger vm session attach --help
2026-04-12 23:48:42 -03:00

76 lines
1.7 KiB
Go

package sessionstream
import (
"encoding/binary"
"encoding/json"
"fmt"
"io"
)
const (
ChannelStdin byte = 0x01
ChannelStdout byte = 0x02
ChannelStderr byte = 0x03
ChannelControl byte = 0x04
FormatV1 = "stdio_mux_v1"
)
type ControlMessage struct {
Type string `json:"type"`
ExitCode *int `json:"exit_code,omitempty"`
Error string `json:"error,omitempty"`
}
func WriteFrame(w io.Writer, channel byte, payload []byte) error {
var header [5]byte
header[0] = channel
binary.BigEndian.PutUint32(header[1:], uint32(len(payload)))
if _, err := w.Write(header[:]); err != nil {
return err
}
if len(payload) == 0 {
return nil
}
_, err := w.Write(payload)
return err
}
func ReadFrame(r io.Reader) (byte, []byte, error) {
var header [5]byte
if _, err := io.ReadFull(r, header[:]); err != nil {
return 0, nil, err
}
length := binary.BigEndian.Uint32(header[1:])
payload := make([]byte, length)
if _, err := io.ReadFull(r, payload); err != nil {
return 0, nil, err
}
return header[0], payload, nil
}
func WriteControl(w io.Writer, message ControlMessage) error {
payload, err := json.Marshal(message)
if err != nil {
return err
}
return WriteFrame(w, ChannelControl, payload)
}
func ReadControl(payload []byte) (ControlMessage, error) {
var message ControlMessage
if err := json.Unmarshal(payload, &message); err != nil {
return ControlMessage{}, err
}
return message, nil
}
func ReadNextControl(r io.Reader) (ControlMessage, error) {
channel, payload, err := ReadFrame(r)
if err != nil {
return ControlMessage{}, err
}
if channel != ChannelControl {
return ControlMessage{}, fmt.Errorf("unexpected channel %d", channel)
}
return ReadControl(payload)
}