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
- type AudioDevice
- type AudioDevices
- type AudioHost
- type AudioStreamConfigRange
- type CM108Device
- type CM108Devices
- type CollectorIssue
- type ConfigItem
- type ConfigSection
- type ErrUnsupportedSchemaVersion
- type Flare
- type GPSCandidate
- type GPSSection
- type LogEntry
- type LogsSection
- type Meta
- type NetworkInterface
- type PTTCandidate
- type PTTSection
- type ServiceStatus
- type SubmitResponse
- type System
- type USBDevice
- type USBTopology
- type User
Constants ¶
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 ¶
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 ¶
func (e ErrUnsupportedSchemaVersion) Error() string
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 ¶
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.