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
76 lines
1.7 KiB
Go
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)
|
|
}
|