flareschema

package
v0.13.13 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 30, 2026 License: GPL-2.0 Imports: 3 Imported by: 0

Documentation

Overview

Package flareschema is the canonical Go definition of the graywolf flare wire payload — the schema-versioned JSON document that the "graywolf flare" CLI submits to graywolf-flare-server.

Contract:

  • SchemaVersion is a monotonic integer; the server accepts and migrates older versions for as long as the build supports them.
  • Every collector section carries its own []CollectorIssue so a failure in one domain never aborts the whole submission.
  • Sub-struct field tags ("json:...,omitempty" where appropriate) are part of the wire contract; renaming or retagging requires a SchemaVersion bump and a corresponding entry in docs/flareschema/.

Cross-repo use:

The graywolf-flare-server lives in its own git repo (~/dev/graywolf-flare-server). It depends on this package via go.mod; during local dev it uses a `replace` directive pointing at this worktree, and CI pins to a tagged graywolf version so the schema and the server can never silently disagree.

Design: .context/2026-04-25-graywolf-flare-system-design.md

§ "Subsystem 2 — graywolf flare CLI" → "Wire Schema".

Index

Constants

View Source
const SchemaVersion = 1

SchemaVersion is the wire schema version this build emits and accepts. Bumping requires:

  • committing a new docs/flareschema/v<N>.json
  • documenting the migration in the flare-server's release notes
  • leaving the prior version's accept path in place until that build is no longer in production

Variables

This section is empty.

Functions

This section is empty.

Types

type AudioDevice

type AudioDevice struct {
	Name             string                   `json:"name"`
	Direction        string                   `json:"direction"`
	IsDefault        bool                     `json:"is_default"`
	Recommended      bool                     `json:"recommended,omitempty"`
	SupportedConfigs []AudioStreamConfigRange `json:"supported_configs,omitempty"`
}

AudioDevice is one cpal device under a host. Direction is "input" or "output". IsDefault marks the host's default-input / default-output pick (each direction has its own default).

Recommended is the flare-side string heuristic only: set when the device's PCM identifier is a stable `plughw:CARD=<name>` form.

For capture devices this intentionally does NOT match the live web picker. The runtime picker probes each device and recommends the PCM form that actually streams (cheap USB chips fail on plughw: and only work via raw hw:). `--list-audio` runs as a separate short-lived process and cannot probe safely, so its Recommended stays a cheap triage hint; the live picker is authoritative. Keep in sync with the doc on graywolf-modem is_recommended_pcm_id and the convergence-test note.

type AudioDevices

type AudioDevices struct {
	Hosts  []AudioHost      `json:"hosts"`
	Issues []CollectorIssue `json:"issues,omitempty"`
}

AudioDevices is the top-level shape emitted by `graywolf-modem --list-audio`. It is also the schema attached to the flare's audio_devices section.

Multi-host: cpal exposes one Host per audio API on the platform (ALSA, PulseAudio, JACK on Linux; CoreAudio on macOS; WASAPI on Windows). We iterate available_hosts() so a JACK-on-Linux user's report carries both ALSA and JACK devices, not whichever host the modem picked at runtime.

type AudioHost

type AudioHost struct {
	ID        string        `json:"id"`
	Name      string        `json:"name"`
	IsDefault bool          `json:"is_default"`
	Devices   []AudioDevice `json:"devices"`
}

AudioHost is one cpal host. ID is the cpal-internal host identifier (e.g. "alsa", "jack", "coreaudio"); Name is the display name from the host trait. IsDefault marks the host cpal::default_host() picked.

type AudioStreamConfigRange

type AudioStreamConfigRange struct {
	Channels        uint16 `json:"channels"`
	MinSampleRateHz uint32 `json:"min_sample_rate_hz"`
	MaxSampleRateHz uint32 `json:"max_sample_rate_hz"`
	SampleFormat    string `json:"sample_format"`
}

AudioStreamConfigRange flattens cpal::SupportedStreamConfigRange into a JSON-friendly shape. Channels is fixed (cpal reports per-config channel count), but sample rate is a range. SampleFormat is one of "i16", "u16", "f32" (the cpal SampleFormat variants).

type CM108Device

type CM108Device struct {
	Path        string `json:"path"`
	Vendor      string `json:"vendor"`
	Product     string `json:"product"`
	Description string `json:"description"`
}

CM108Device matches the shape emitted by `graywolf-modem --list-cm108` today (graywolf-modem/src/cm108.rs:11). Field names and JSON tags MUST stay in lockstep with that file — this is the cross-language contract for the existing flag, not a new one.

type CM108Devices

type CM108Devices struct {
	Devices []CM108Device    `json:"devices"`
	Issues  []CollectorIssue `json:"issues,omitempty"`
}

CM108Devices wraps the modem's bare array output into the same section-with-issues shape every other flare section uses, so the operator UI can render it uniformly. The collector side fills Devices from the modem's array output and Issues from any exec failure.

type CollectorIssue

type CollectorIssue struct {
	Kind    string `json:"kind"`
	Message string `json:"message"`
	Path    string `json:"path,omitempty"`
}

CollectorIssue is appended to a section's Issues slice when a collector hits a recoverable problem (missing permission, absent dependency, unparseable system file). It is intentionally shape-stable across sections so the operator UI can render every section's issues with one component.

Kind is a short snake_case tag (e.g. "permission_denied", "modem_unavailable", "parse_failed") that the operator UI groups by. Message is a free-text human-readable explanation. Path is the filesystem path or device the collector tripped over, when applicable.

type ConfigItem

type ConfigItem struct {
	Key   string `json:"key"`
	Value string `json:"value"`
}

ConfigItem is one row of the configstore dump after scrubbing. Values that the scrubber decided to suppress are written as the literal string "[REDACTED]" — that placeholder lives in the value field so the operator UI doesn't need a separate "redacted" flag column.

type ConfigSection

type ConfigSection struct {
	Items  []ConfigItem     `json:"items"`
	Issues []CollectorIssue `json:"issues,omitempty"`
}

ConfigSection is the top-level "config" object on a flare. Items preserve the order the configstore emitted them so flares from the same install stay comparable across re-runs. Issues records collector failures (e.g. "configstore unreadable") so a flare without config remains useful for system+device debugging.

type ErrUnsupportedSchemaVersion

type ErrUnsupportedSchemaVersion struct {
	Got int
}

ErrUnsupportedSchemaVersion is returned by Unmarshal when the payload's schema_version is greater than this build's SchemaVersion. Older versions are intentionally not rejected here — the server is responsible for migrating them up.

func (ErrUnsupportedSchemaVersion) Error

type Flare

type Flare struct {
	SchemaVersion int           `json:"schema_version"`
	User          User          `json:"user"`
	Meta          Meta          `json:"meta"`
	Config        ConfigSection `json:"config"`
	System        System        `json:"system"`
	ServiceStatus ServiceStatus `json:"service_status"`
	PTT           PTTSection    `json:"ptt"`
	GPS           GPSSection    `json:"gps"`
	AudioDevices  AudioDevices  `json:"audio_devices"`
	USBTopology   USBTopology   `json:"usb_topology"`
	CM108         CM108Devices  `json:"cm108"`
	Logs          LogsSection   `json:"logs"`
}

Flare is the top-level wire payload posted to /api/v1/submit on graywolf-flare-server.

Field order in this struct is the canonical wire order. encoding/json preserves struct order on Marshal, so the resulting JSON is stable across builds — useful for the operator UI comparing multiple flares from the same install side-by-side and for human-readable dry-run output.

schema_version is duplicated here at the top level (and lives inside Meta as well). The top-level field is the contract surface every receiver checks first; Meta.SchemaVersion mirrors it so a flare payload separated from its envelope (e.g. a dry-run save-to-file the operator emails) is still self-describing.

func BuildSampleFlare

func BuildSampleFlare() Flare

BuildSampleFlare returns a fully populated Flare suitable for round trip tests, schema-generation tooling, and dev fixtures. Every section has at least one item AND at least one CollectorIssue so the schema generator visits every branch of the struct tree, and so the round-trip test exercises every field's JSON tags.

The sample is deliberately deterministic — no random or time-based values — so committing the generated schema document doesn't churn on each regeneration.

func Unmarshal

func Unmarshal(data []byte) (*Flare, error)

Unmarshal decodes a flare payload, rejecting payloads whose schema_version is greater than this build's SchemaVersion. Older payloads pass through unchanged — migrating them is the receiver's job, not this package's.

The two-pass decode is deliberate: peeking at the version header first lets us return ErrUnsupportedSchemaVersion before attempting to fit a future-shaped payload into our current Flare struct, which would otherwise produce confusing partial-decode results.

type GPSCandidate

type GPSCandidate struct {
	Kind        string `json:"kind"`
	Path        string `json:"path,omitempty"`
	Vendor      string `json:"vendor,omitempty"`
	Product     string `json:"product,omitempty"`
	Description string `json:"description,omitempty"`
	Permissions string `json:"permissions,omitempty"`
	Owner       string `json:"owner,omitempty"`
	Group       string `json:"group,omitempty"`
	Reachable   bool   `json:"reachable,omitempty"`
	Accessible  bool   `json:"accessible,omitempty"`
}

GPSCandidate is a serial device that looked GPS-ish, or the gpsd socket. Kind is "serial" or "gpsd_socket". Reachable applies only to the socket case (was it accepting connections at probe time).

type GPSSection

type GPSSection struct {
	Candidates []GPSCandidate   `json:"candidates"`
	Issues     []CollectorIssue `json:"issues,omitempty"`
}

GPSSection wraps the candidate list with an issues slice.

type LogEntry

type LogEntry struct {
	TsNs      int64          `json:"ts_ns"`
	Level     string         `json:"level"`
	Component string         `json:"component"`
	Msg       string         `json:"msg"`
	Attrs     map[string]any `json:"attrs,omitempty"`
}

LogEntry mirrors one row of the logbuffer ring (graywolf-logs.db). The shape matches pkg/logbuffer's persisted columns one-for-one: ts_ns is unix nanoseconds, Level is "DEBUG"|"INFO"|"WARN"|"ERROR", Component is the dotted slog group chain.

Attrs is map[string]any (not json.RawMessage) so the operator UI can render it as a key/value table without re-parsing JSON. Empty maps are dropped via omitempty so a record with no attrs doesn't carry an extra "attrs":{} on the wire.

type LogsSection

type LogsSection struct {
	Source  string           `json:"source"`
	Entries []LogEntry       `json:"entries"`
	Issues  []CollectorIssue `json:"issues,omitempty"`
}

LogsSection is the top-level "logs" object on a flare. Source documents where the rows came from ("graywolf-logs.db", "journalctl", "Console.app", "EventLog") so a future operator can identify the collector path even after schema evolution.

type Meta

type Meta struct {
	SchemaVersion        int       `json:"schema_version"`
	GraywolfVersion      string    `json:"graywolf_version"`
	GraywolfCommit       string    `json:"graywolf_commit,omitempty"`
	GraywolfModemVersion string    `json:"graywolf_modem_version,omitempty"`
	GraywolfModemCommit  string    `json:"graywolf_modem_commit,omitempty"`
	HostnameHash         string    `json:"hostname_hash,omitempty"`
	SubmittedAt          time.Time `json:"submitted_at"`
}

Meta carries provenance for the submission: which graywolf and graywolf-modem build produced it, the schema version of the payload, the (irreversible, hashed) hostname, and the submission timestamp.

Both the Go binary and the Rust binary carry their own version+commit because a "dev-install mismatch" — operator running a freshly built Go graywolf against a stale graywolf-modem — is a known support pattern that we want to spot in the UI without extra collector logic.

type NetworkInterface

type NetworkInterface struct {
	Name   string   `json:"name"`
	MACOUI string   `json:"mac_oui,omitempty"`
	Up     bool     `json:"up"`
	IPv4   []string `json:"ipv4,omitempty"`
	IPv6   []string `json:"ipv6,omitempty"`
	MTU    int      `json:"mtu,omitempty"`
}

NetworkInterface carries the OUI-only identity of a NIC plus its configured addresses. The full MAC is intentionally not represented here; the OUI alone is enough to identify USB-Ethernet adapters or Pi-built-in NICs.

IPv4/IPv6 are recorded as CIDR strings ("192.168.1.5/24", "fe80::1/64") so an operator can see at a glance which subnet the radio host is on. Loopback (127.0.0.1, ::1) is filtered out at the collector. MTU is the kernel-reported MTU; 0 means the value was unavailable.

type PTTCandidate

type PTTCandidate struct {
	Kind        string `json:"kind"`
	Path        string `json:"path,omitempty"`
	Vendor      string `json:"vendor,omitempty"`
	Product     string `json:"product,omitempty"`
	Description string `json:"description,omitempty"`
	Permissions string `json:"permissions,omitempty"`
	Owner       string `json:"owner,omitempty"`
	Group       string `json:"group,omitempty"`
	Accessible  bool   `json:"accessible,omitempty"`
}

PTTCandidate is one device the PTT enumerator considered. The exact fields populated depend on Kind:

  • "serial": Path, Vendor, Product, Description, Permissions, Owner, Group
  • "cm108_hid": Path, Vendor, Product, Description (interface number lives in CM108Devices)
  • "gpio_chip": Path, Description; Permissions+Owner+Group when readable
  • "parport": Path, Description

Accessible is the result of an actual open-for-write probe — it is the most actionable bit for triage because "user is not in dialout/gpio" shows up here as Accessible=false even when Permissions/Owner/Group look right.

type PTTSection

type PTTSection struct {
	Candidates []PTTCandidate   `json:"candidates"`
	Issues     []CollectorIssue `json:"issues,omitempty"`
}

PTTSection wraps the candidate list with an issues slice for collector failures (e.g. /dev/parport* unreadable).

type ServiceStatus

type ServiceStatus struct {
	Manager      string           `json:"manager,omitempty"`
	Unit         string           `json:"unit,omitempty"`
	IsActive     bool             `json:"is_active"`
	IsFailed     bool             `json:"is_failed"`
	RestartCount int              `json:"restart_count,omitempty"`
	Issues       []CollectorIssue `json:"issues,omitempty"`
}

ServiceStatus reports the platform's view of the graywolf service. Manager is one of "systemd", "launchd", "sc" (Windows), or empty when no service supervisor is detected. Empty Manager + a populated Issues slice is the "graywolf is being run interactively" case.

IsActive and IsFailed are NOT omitempty: false is a diagnostically load-bearing value (the operator wants to see "service is not running" surfaced explicitly, not inferred from absence). When no supervisor is detected, the operator UI keys off Manager == "" and ignores these fields.

type SubmitResponse

type SubmitResponse struct {
	FlareID       string `json:"flare_id"`
	PortalToken   string `json:"portal_token"`
	PortalURL     string `json:"portal_url"`
	SchemaVersion int    `json:"schema_version"`
}

SubmitResponse is the JSON document graywolf-flare-server returns from POST /api/v1/submit.

FlareID is the server-assigned UUID; PortalToken is the unguessable per-flare URL token the server may use to authenticate operator-portal access (the CLI does not persist it locally); PortalURL is the fully-qualified browser link the CLI prints (and the server emails, when --email was supplied).

SchemaVersion echoes the schema_version the server accepted — useful for diagnostics if the CLI ever finds itself talking to a server running a different build than the docs/flareschema/ commit the CLI was built against.

type System

type System struct {
	OS                string             `json:"os"`
	OSPretty          string             `json:"os_pretty,omitempty"`
	Kernel            string             `json:"kernel,omitempty"`
	Arch              string             `json:"arch"`
	IsRaspberryPi     bool               `json:"is_raspberry_pi,omitempty"`
	PiModel           string             `json:"pi_model,omitempty"`
	Groups            []string           `json:"groups,omitempty"`
	NTPSynchronized   bool               `json:"ntp_synchronized,omitempty"`
	UdevRulesPresent  []string           `json:"udev_rules_present,omitempty"`
	NetworkInterfaces []NetworkInterface `json:"network_interfaces,omitempty"`
	Issues            []CollectorIssue   `json:"issues,omitempty"`
}

System is the OS + hardware + identity snapshot collected at flare time. Field set is the union across Linux, macOS, and Windows; per-OS collectors populate the subset they can fill in and leave the rest at zero. omitempty on the platform-conditional fields keeps macOS/Windows payloads from carrying noisy "is_raspberry_pi=false" defaults.

Privacy: hostnames are SHA256-truncated-to-8 before they reach this struct (see Meta.HostnameHash). Network interfaces carry the MAC OUI only — never the full address — to identify the hardware vendor without the per-device suffix.

type USBDevice

type USBDevice struct {
	BusNumber      int    `json:"bus_number"`
	PortPath       string `json:"port_path,omitempty"`
	VendorID       string `json:"vendor_id"`
	ProductID      string `json:"product_id"`
	VendorName     string `json:"vendor_name,omitempty"`
	ProductName    string `json:"product_name,omitempty"`
	Manufacturer   string `json:"manufacturer,omitempty"`
	Serial         string `json:"serial,omitempty"`
	Class          string `json:"class,omitempty"`
	Subclass       string `json:"subclass,omitempty"`
	USBVersion     string `json:"usb_version,omitempty"`
	Speed          string `json:"speed,omitempty"`
	MaxPowerMA     int    `json:"max_power_ma,omitempty"`
	HubPowerSource string `json:"hub_power_source,omitempty"`
}

USBDevice is one device entry in the topology. Vendor/product IDs are rendered as zero-padded 4-digit lowercase hex (e.g. "0d8c") to match the convention used by the rest of graywolf's USB-aware code (see existing graywolf-modem CM108Device.vendor at cm108.rs:52).

Speed is one of "low", "full", "high", "super", "super_plus", or "unknown" — the nusb Speed enum collapsed to lower-snake-case.

HubPowerSource is "bus", "self", or "unknown" — readable from the hub descriptor only on Linux today; macOS/Windows leave it as "unknown".

type USBTopology

type USBTopology struct {
	Devices []USBDevice      `json:"devices"`
	Issues  []CollectorIssue `json:"issues,omitempty"`
}

USBTopology is the top-level shape emitted by `graywolf-modem --list-usb`. It walks every device the kernel exposes and records the minimum needed to diagnose:

  • "is the radio interface plugged in" (vendor/product strings)
  • "is it on a powered hub" (hub_power_source, max_power_ma)
  • "is it negotiating a sane speed" (speed, usb_version)
  • "what bus position" (port_path) so two identical devices can be told apart in a single flare and across re-submissions

type User

type User struct {
	Email          string `json:"email,omitempty"`
	Notes          string `json:"notes,omitempty"`
	RadioModel     string `json:"radio_model,omitempty"`
	AudioInterface string `json:"audio_interface,omitempty"`
}

User holds the optional free-form fields the operator gave the CLI via flags. Every field is omitempty — a user submitting anonymously sends {} here.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL