Files
Maven/internal/ipc/frame.go
kami d52f60c54e maven: fix test mocks for CalendarEvents interface (verification)
- Add CalendarEvents method to recordingAPI in auth_test.go
- Add CalendarEvents method to fakeCore in handlers_test.go

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:20:16 +04:00

87 lines
3.3 KiB
Go

// Package ipc is maven's core↔module boundary.
//
// Core = the only key-holder: the daemon process holds the unlocked sqlcipher
// db + the trigger loop. Modules (stt/tts, router/classifier, tool executors,
// delivery) are separate processes — restart-free, key-free, fail-independent.
// "a crashing tts can't read the key page" only holds if there IS a page
// boundary between core and modules; this package IS that boundary.
//
// Transport: unix domain socket, local-only. The socket's filesystem perms
// (0700 dir, 0600 socket) are the current auth floor — "same unix user" —
// carrying the same instinct as wg-floor at the network radius. The full
// 4-layer auth cascade (wg/mTLS/passkey/step-up) is a later module; IPC
// threads a Caller (uid/pid via SO_PEERCRED) so the auth layer can scope
// module authority without restructuring the wire.
//
// Core mediates, never hands back a db handle. The methods here are the only
// state operations a module can perform: write a fact (provenance-scoped by
// source), read a fact / presence, create/complete reminders, record/resolve
// nudges. Anything needing raw db access lives in core and is unreachable.
package ipc
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
)
// maxFrame — 4 MiB. a single fact/reminder/nudge is tiny; this is a safety cap
// against a confused peer sending a terabyte of length prefix, not a real
// operational limit. away-channel bodies are minimal by spec.
const maxFrame = 4 << 20
// ErrFrameTooLarge — a frame exceeded maxFrame; the conn is now desynchronized
// (we read the length but not the body), so the caller must close it.
var ErrFrameTooLarge = errors.New("ipc: frame too large")
// writeFrame encodes v as JSON and frames it as a 4-byte big-endian length
// prefix + body. length-prefixed JSON (not a tighter binary schema) is the
// deferred-but-picked wire format: debuggable with `socat`/`nc`, trivial to
// evolve while the protocol settles, and at single-user local scale the
// encode cost is invisible next to a db round-trip.
func writeFrame(w io.Writer, v any) error {
body, err := json.Marshal(v)
if err != nil {
return fmt.Errorf("ipc: marshal frame: %w", err)
}
if len(body) > maxFrame {
return fmt.Errorf("%w: %d bytes", ErrFrameTooLarge, len(body))
}
var hdr [4]byte
binary.BigEndian.PutUint32(hdr[:], uint32(len(body)))
if _, err := w.Write(hdr[:]); err != nil {
return fmt.Errorf("ipc: write frame header: %w", err)
}
if _, err := w.Write(body); err != nil {
return fmt.Errorf("ipc: write frame body: %w", err)
}
return nil
}
// readFrame reads one length-prefixed frame into v. A zero-length frame is
// legal JSON (e.g. `null`/`{}` encode to a few bytes, never zero) — we don't
// treat it as EOF; only an EOF on the header read does.
func readFrame(r io.Reader, v any) error {
var hdr [4]byte
if _, err := io.ReadFull(r, hdr[:]); err != nil {
if errors.Is(err, io.EOF) {
return io.EOF
}
return fmt.Errorf("ipc: read frame header: %w", err)
}
n := binary.BigEndian.Uint32(hdr[:])
if n > maxFrame {
return fmt.Errorf("%w: %d bytes", ErrFrameTooLarge, n)
}
buf := make([]byte, n)
if _, err := io.ReadFull(r, buf); err != nil {
return fmt.Errorf("ipc: read frame body: %w", err)
}
if err := json.Unmarshal(buf, v); err != nil {
return fmt.Errorf("ipc: unmarshal frame: %w", err)
}
return nil
}