model

package
v0.2.23 Latest Latest
Warning

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

Go to latest
Published: Aug 28, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Index

Constants

View Source
const (
	TaskQueued   = "queued"
	TaskLeased   = "leased"
	TaskFinished = "finished"
	TaskFailed   = "failed"
	// TaskCancelled is an operator-cancelled task. Only a queued task can be
	// cancelled (a leased task is already running on the agent and cannot be
	// reliably stopped from the server); it is then never leased.
	TaskCancelled = "cancelled"

	TerminalPending = "pending"
	TerminalOpen    = "open"
	TerminalClosed  = "closed"
	TerminalFailed  = "failed"

	ApprovalPending  = "pending"
	ApprovalApproved = "approved"
	ApprovalRejected = "rejected"
	ApprovalApplied  = "applied"

	PluginStatusVerified  = "verified"
	PluginStatusInstalled = "installed"
	PluginStatusActive    = "active"
	PluginStatusDisabled  = "disabled"
)
View Source
const (
	NodeIPModeAuto     = "auto"     // static override if set, else resolver probe
	NodeIPModeStatic   = "static"   // use the operator-provided static IPs only
	NodeIPModeResolver = "resolver" // always probe the resolvers, ignore static
	NodeIPModeScript   = "script"   // run an operator-provided script on the agent
)
View Source
const (
	RenewalCycleMonthly    = "monthly"
	RenewalCycleQuarterly  = "quarterly"
	RenewalCycleSemiannual = "semiannual"
	RenewalCycleAnnual     = "annual"
	RenewalCycleCustomDays = "custom_days"
)
View Source
const (
	DNSEngineCoreDNS = "coredns"

	DNSExposureMesh   = "mesh"
	DNSExposurePublic = "public"

	DNSZoneForward = "forward"
	DNSZoneStatic  = "static"
	DNSZoneBlock   = "block"

	DNSStatusPending  = "pending"
	DNSStatusApplying = "applying"
	DNSStatusRunning  = "running"
	DNSStatusFailed   = "failed"
	DNSStatusDisabled = "disabled"
)
View Source
const (
	GeoRoutingStrategyGeoIP      = "geoip"
	GeoRoutingStrategyAllHealthy = "all-healthy"
)
View Source
const (
	NetRuleAllow = "allow"
	NetRuleDeny  = "deny"

	NetDirEgress  = "egress"
	NetDirIngress = "ingress"

	NetProtoTCP = "tcp"
	NetProtoUDP = "udp"
	NetProtoAny = "any"

	NetRefNode   = "node"
	NetRefCIDR   = "cidr"
	NetRefDomain = "domain"
	NetRefAny    = "any"
	// NetRefGroup is a group-scoped remote. It is an authoring-layer kind only:
	// the server expands it to one concrete node ref per resolved group member
	// before compilation, so the per-node nft compiler never sees a group ref.
	NetRefGroup = "group"
)
View Source
const (
	// NetRefZone is a guard-zone-scoped remote (design-13). Like NetRefGroup it
	// is an authoring-layer kind: the server resolves the zone to concrete
	// interface/CIDR matches before the per-node nft compiler runs.
	NetRefZone = "zone"

	GuardProtoICMP   = "icmp"
	GuardProtoICMPv6 = "icmpv6"

	// Builtin guard-zone ids. "public" resolves to the node's public-facing
	// interface, "loopback" is the always-accepted lo scaffold, and overlay
	// zones ("wireguard", "tailscale") resolve to the node's discovered or
	// managed overlay interfaces/CIDRs.
	GuardZonePublic    = "public"
	GuardZoneLoopback  = "loopback"
	GuardZoneWireGuard = "wireguard"
	GuardZoneTailscale = "tailscale"
)
View Source
const (
	// WireGuard topology modes (design-13 §5.3). Mesh is today's implicit
	// behavior; hub-and-spoke and custom are additive.
	WGTopologyMesh     = "mesh"
	WGTopologyHubSpoke = "hub-and-spoke"
	WGTopologyCustom   = "custom"

	WGRoleHub   = "hub"
	WGRoleSpoke = "spoke"
	WGRolePeer  = "peer"

	WGDefaultListenPort = 51820
	WGDefaultKeepalive  = 25
)
View Source
const (
	ProxyCoreSingbox = "sing-box"
	ProxyCoreXray    = "xray"

	ProxyProtocolVLESS       = "vless"
	ProxyProtocolVMess       = "vmess"
	ProxyProtocolTrojan      = "trojan"
	ProxyProtocolShadowsocks = "shadowsocks"
	ProxyProtocolHysteria2   = "hysteria2"

	ProxyTransportTCP   = "tcp"
	ProxyTransportWS    = "ws"
	ProxyTransportGRPC  = "grpc"
	ProxyTransportHTTP2 = "http2"
	ProxyTransportQUIC  = "quic"

	ProxySecurityNone    = "none"
	ProxySecurityTLS     = "tls"
	ProxySecurityReality = "reality"

	ProxyUserStatusActive    = "active"
	ProxyUserStatusExpired   = "expired"
	ProxyUserStatusOverQuota = "over_quota"
	ProxyUserStatusDisabled  = "disabled"

	ProxyUsageCollectorStatusOK    = "ok"
	ProxyUsageCollectorStatusError = "error"
)
View Source
const (
	APIErrorBadRequest       = "bad_request"
	APIErrorUnauthorized     = "unauthorized"
	APIErrorForbidden        = "forbidden"
	APIErrorNotFound         = "not_found"
	APIErrorMethodNotAllowed = "method_not_allowed"
	APIErrorRateLimited      = "rate_limited"
	APIErrorBadGateway       = "bad_gateway"
	APIErrorInternal         = "internal_error"
	APIErrorRequestFailed    = "request_failed"

	APIErrorCapabilityDenied        = "capability_denied"
	APIErrorApprovalStale           = "approval_stale"
	APIErrorAgentUpdateNoop         = "agent_update_noop"
	APIErrorMFARequired             = "mfa_required"
	APIErrorInvalidNodeToken        = "invalid_node_token"
	APIErrorInvalidTaskLease        = "invalid_task_lease"
	APIErrorTaskOutputLimitExceeded = "task_output_limit_exceeded"
)
View Source
const (
	StorageKindKV     = "kv"
	StorageKindStatic = "static"

	StorageAccessAdmin = "admin"
	StorageAccessRead  = "read"
	StorageAccessWrite = "write"
)
View Source
const (
	DDNSProviderCloudflare = "cloudflare"
	DDNSProviderWebhook    = "webhook"
)
View Source
const (
	MonitorTypeTCP  = "tcp"
	MonitorTypeHTTP = "http"
	MonitorTypeICMP = "icmp"
)
View Source
const (
	// ShareSourceCoreProxyUser renders from a proxy user the core already owns.
	ShareSourceCoreProxyUser = "core.proxy_user"
	// ShareSourcePlugin asks a plugin to produce the body. The plugin never sees
	// the share's token and never owns the route.
	ShareSourcePlugin = "plugin"
)
View Source
const (
	SubscriptionSourceManifestSchemaV1 = "lattice.subscription-source-manifest.v1"
	SubscriptionSourceRendererV1       = "vpn-core-graph-v1"
	MaxSubscriptionSourceRoots         = 2_048
	MaxSubscriptionSourceVisits        = 10_000
	MaxSubscriptionURIBytes            = 4 << 10
	MaxSubscriptionSourceManifestBytes = 1 << 20
	MaxSubscriptionRawBytes            = 1 << 20
	MaxSubscriptionResponseBytes       = 4 << 20
)
View Source
const (
	TraceSessionRunning = "running"
	TraceSessionExpired = "expired"
	TraceSessionStopped = "stopped"
)

Trace session states.

View Source
const (
	CloseEOF             = "eof"              // upload/download finished
	CloseCanceled        = "canceled"         // closed, no error (trace level)
	CloseReset           = "reset"            // connection reset by peer
	CloseTimeout         = "timeout"          // i/o timeout
	CloseDialFailed      = "dial_failed"      // open connection to ... using outbound/...
	CloseAuthFailed      = "auth_failed"      // process connection from ... (no user is known)
	CloseHandshakeFailed = "handshake_failed" // TLS handshake / upload handshake
	CloseUDPIdle         = "udp_idle"         // packet upload closed, no error
	CloseCoreRestart     = "core_restart"     // core generation changed under an open connection
	// CloseUnknown is an honest gap: the stream ended, or the id never produced
	// a terminal line. It must never be rendered as a clean close.
	CloseUnknown = "unknown"
)

How a connection ended. Derived from the last line sing-box emitted for it; see SINGBOX-TRACE-DESIGN.md section 4.6 for the mapping.

View Source
const (
	UserKindManaged    = "managed"    // u_<16hex>, reversible to a Lattice user
	UserKindLegacy     = "legacy"     // a free-text operator label on a legacy ProxyUser
	UserKindDiscovered = "discovered" // a third-party adopted node's named user
	UserKindUnnamed    = "unnamed"    // sing-box logged an index, not a name
	// UserKindUnobserved means the identity was never delivered for this
	// connection, which is different from sing-box declining to name a user.
	//
	// The case that matters is multiplexing: a VLESS mux transport
	// authenticates the user on the OUTER connection, and sing-box then mints a
	// fresh log id for every inner stream. The inner streams begin at routing
	// or outbound, so no user-bearing line ever carries their id, and the Clash
	// API does not serialise the user either. The evidence does not exist under
	// that id, so calling it unnamed would blame sing-box for something it was
	// never asked. It also covers a collector that started mid-connection.
	UserKindUnobserved = "unobserved"
	UserKindUnresolved = "unresolved" // a name that no lookup could place, yet
)

How confidently a user was attributed to a connection.

View Source
const (
	HopConfidenceExact     = "exact"     // identity carried through the chain (carry_identity)
	HopConfidenceInferred  = "inferred"  // one candidate matched dst + window + declared edge
	HopConfidenceAmbiguous = "ambiguous" // several candidates matched; Candidates lists them
	HopConfidenceNone      = "none"      // no downstream record matched
)

How a hop path was joined across machines.

View Source
const (
	MarkerCoreRestart = "core_restart" // sing-box restarted; Count is connections swept
	MarkerConfigApply = "config_apply" // an approved apply landed on the node
	MarkerSubFetch    = "sub_fetch"    // a subscription URL was fetched by a user
	MarkerSession     = "session"      // a trace session started or ended
)

TraceMarkerKind classifies the events drawn on the trace timeline. Each comes from a signal Lattice already records; none is new instrumentation.

View Source
const (
	ApprovalStaleAgentUpdatePolicyChanged = "agent_update_policy_changed"
)
View Source
const SubscriptionShareSchemaVersion = 1

SubscriptionShareSchemaVersion is the current record shape. Readers must tolerate a higher value rather than refuse the record: migrations are additive only, so a newer record is readable, and Extra preserves what this version does not name.

View Source
const SubscriptionSnapshotSchemaVersion = 2

SubscriptionSnapshotSchemaVersion is the current record shape.

Variables

This section is empty.

Functions

func CanonicalSubscriptionSourceManifest added in v0.2.19

func CanonicalSubscriptionSourceManifest(manifest SubscriptionSourceManifestV1) ([]byte, string, error)

func SnapshotKey added in v0.2.19

func SnapshotKey(pluginID, subscriptionID string) string

SnapshotKey is the storage key for one subscription's snapshot.

func SubscriptionSourceVersion added in v0.2.19

func SubscriptionSourceVersion(canonical []byte) string

func TraceLevelAtLeast added in v0.2.19

func TraceLevelAtLeast(have, want TraceLevel) bool

TraceLevelAtLeast reports whether have is at least as verbose as want.

func ValidTraceLevel added in v0.2.19

func ValidTraceLevel(l TraceLevel) bool

ValidTraceLevel reports whether l is one of the three known levels. Callers fail closed on false rather than defaulting, because silently collecting at the wrong verbosity is both a cost and a privacy surprise.

Types

type APIError added in v0.2.0

type APIError struct {
	Code      string `json:"code"`
	Message   string `json:"message"`
	RequestID string `json:"request_id"`
}

APIError is the stable machine-readable error shape shared by server, dashboard, agents and plugins. Messages are user-facing; callers should make authorization and retry decisions from Code.

type APIErrorResponse added in v0.2.0

type APIErrorResponse struct {
	Error APIError `json:"error"`
}

type AgentConfig added in v0.2.1

type AgentConfig struct {
	Debug AgentDebugConfig `json:"debug"`
	// LatticeIdentityUUID is the server-minted, stable node identity used by
	// line discovery metadata and cross-node line graph resolution. Empty means
	// the server has not minted one yet; old agents ignore this field.
	LatticeIdentityUUID string `json:"lattice_identity_uuid,omitempty"`
	// TerminalTransport is the server's per-node override for the agent terminal
	// transport: "poll" or "stream". Empty means "no override" — the agent keeps
	// its startup -terminal-transport / LATTICE_TERMINAL_TRANSPORT value. This is
	// the rollout lever for promoting streaming per node without a redeploy; it
	// affects only sessions opened after the change, never in-flight ones.
	TerminalTransport string `json:"terminal_transport,omitempty"`
	// IPConfig is the server's per-node override for public-IP discovery. nil
	// means "no override" — the agent keeps its startup -ip-mode/-ip-resolvers
	// flags. Old agents that do not know this field ignore it safely.
	IPConfig *NodeIPConfig `json:"ip_config,omitempty"`
}

type AgentDebugBatch added in v0.2.1

type AgentDebugBatch struct {
	NodeID     string    `json:"node_id"`
	Lines      []string  `json:"lines"`
	CapturedAt time.Time `json:"captured_at"`
}

AgentDebugBatch carries locally emitted agent diagnostics to the server log store when server-side collection is enabled for the node.

type AgentDebugConfig added in v0.2.1

type AgentDebugConfig struct {
	Enabled       bool `json:"enabled"`
	Collect       bool `json:"collect"`
	MaxLineBytes  int  `json:"max_line_bytes,omitempty"`
	MaxBatchLines int  `json:"max_batch_lines,omitempty"`
}

AgentDebugConfig is the runtime policy an agent polls from the server. It is intentionally small and non-secret so older agents can ignore it safely.

type AgentDebugPolicy added in v0.2.1

type AgentDebugPolicy struct {
	Enabled   bool      `json:"enabled"`
	Collect   bool      `json:"collect"`
	UpdatedAt time.Time `json:"updated_at,omitempty"`
}

AgentDebugPolicy is the operator-owned diagnostic mode for a node-agent. When Enabled is true the agent emits verbose non-secret diagnostics locally on the node. Collect controls whether those diagnostics are also shipped back to the server log store. Server-managed debug collection defaults to true when enabled, but operators may keep local node debug output without central collection by setting Collect=false.

type AgentLaunchConfig added in v0.2.10

type AgentLaunchConfig struct {
	AllowExec             bool   `json:"allow_exec"`
	AllowRootExec         bool   `json:"allow_root_exec"`
	NoExec                bool   `json:"no_exec"`
	AllowTerminal         bool   `json:"allow_terminal"`
	TerminalTransport     string `json:"terminal_transport,omitempty"`
	SSHAlerts             bool   `json:"ssh_alerts"`
	SingBoxDiscover       bool   `json:"singbox_discover"`
	SingBoxBin            string `json:"singbox_bin,omitempty"`
	ProxyUsageFile        string `json:"proxy_usage_file,omitempty"`
	ProxyUsageURL         string `json:"proxy_usage_url,omitempty"`
	ProxyUsageXrayAPI     string `json:"proxy_usage_xray_api,omitempty"`
	ProxyUsageXrayBin     string `json:"proxy_usage_xray_bin,omitempty"`
	ProxyUsageXrayPattern string `json:"proxy_usage_xray_pattern,omitempty"`
	// SingBoxStatsAPI is the loopback host:port of the sing-box experimental
	// stats API (ADR-004); empty disables the singbox-stats collector.
	SingBoxStatsAPI string    `json:"singbox_stats_api,omitempty"`
	UpdatedAt       time.Time `json:"updated_at,omitempty"`
}

AgentLaunchConfig mirrors lattice-agent startup flags/env vars used by the dashboard's enroll/reconfigure command generator. Runtime overrides that already-running agents poll remain in AgentConfig instead.

type AgentUpdatePolicy added in v0.2.0

type AgentUpdatePolicy struct {
	NodeID             string    `json:"node_id"`
	Enabled            bool      `json:"enabled"`
	AutoPlan           bool      `json:"auto_plan"`
	TargetVersion      string    `json:"target_version"`
	BinaryURL          string    `json:"binary_url"`
	SHA256             string    `json:"sha256"`
	InstallPath        string    `json:"install_path"`
	ServiceName        string    `json:"service_name"`
	LastPlannedVersion string    `json:"last_planned_version,omitempty"`
	LastPlannedAt      time.Time `json:"last_planned_at,omitempty"`
	LastAppliedVersion string    `json:"last_applied_version,omitempty"`
	LastAppliedAt      time.Time `json:"last_applied_at,omitempty"`
	LastError          string    `json:"last_error,omitempty"`
	CreatedAt          time.Time `json:"created_at"`
	UpdatedAt          time.Time `json:"updated_at"`
}

AgentUpdatePolicy is a server-owned node-agent update intent. It carries no secrets: operators provide a public HTTPS binary URL plus the expected SHA-256 digest, and the server turns that into a reviewed, plan-hash-bound update task. AutoPlan never mutates a node directly; it only creates a pending approval when the node reports a different AgentVersion and no equivalent pending/approved update is already open.

type Approval

type Approval struct {
	ID         string    `json:"id"`
	NodeID     string    `json:"node_id"`
	Plugin     string    `json:"plugin"`
	Action     string    `json:"action"`
	Plan       string    `json:"plan"`
	Status     string    `json:"status"`
	Reason     string    `json:"reason,omitempty"`
	Stale      bool      `json:"stale,omitempty"`
	StaleCode  string    `json:"stale_code,omitempty"`
	ActorID    string    `json:"actor_id"`
	ApprovedBy string    `json:"approved_by,omitempty"`
	CreatedAt  time.Time `json:"created_at"`
	UpdatedAt  time.Time `json:"updated_at"`

	// Operation binding (spec §9.3). When a plugin's plan-effect method produces a
	// host-risk plan, the approval records the exact code and inputs that produced it,
	// so execution can refuse to run against anything that has since changed. These are
	// typed columns rather than values buried in Plan: each is compared to live state at
	// execute time, and each is what the operator's review is accountable for.
	//
	// All omitempty, so an approval that is not a plugin operation (nft, dns, agent
	// update) serializes exactly as before.
	PluginVersion  string `json:"plugin_version,omitempty"`
	ArtifactDigest string `json:"artifact_digest,omitempty"`
	Service        string `json:"service,omitempty"`
	Method         string `json:"method,omitempty"`
	// RequestSHA256 hashes the request that produced the plan. Re-planning with
	// different inputs yields a different approval; an approval cannot be replayed
	// against a request the operator never saw.
	RequestSHA256 string `json:"request_sha256,omitempty"`
	// Targets is the full node set the operation would touch. NodeID stays as the first
	// target so the existing per-node approval machinery keeps working unchanged.
	Targets []string `json:"targets,omitempty"`
}

type AuditEvent

type AuditEvent struct {
	ID            string            `json:"id"`
	At            time.Time         `json:"at"`
	ActorID       string            `json:"actor_id"`
	TokenID       string            `json:"token_id"`
	NodeID        string            `json:"node_id"`
	Action        string            `json:"action"`
	Scope         string            `json:"scope"`
	Decision      string            `json:"decision"`
	Reason        string            `json:"reason"`
	CorrelationID string            `json:"correlation_id"`
	Metadata      map[string]string `json:"metadata,omitempty"`
}

type ConnRecord added in v0.2.19

type ConnRecord struct {
	// Identity of where it happened.
	NodeID      string `json:"node_id"`
	LineUUID    string `json:"line_uuid,omitempty"`
	LineHashID  string `json:"line_hash_id,omitempty"`
	InboundTag  string `json:"inbound_tag,omitempty"`
	InboundType string `json:"inbound_type,omitempty"`

	// Identity of who. UserName is what sing-box logged; UserID is the Lattice
	// user it was reversed to, empty unless UserKind is managed.
	UserName string `json:"user_name,omitempty"`
	UserID   string `json:"user_id,omitempty"`
	UserKind string `json:"user_kind,omitempty"`

	// The connection itself. LogID is sing-box's per-connection id: uint32 from
	// rand, unique enough within a process-lifetime window but NOT globally, so
	// it is a join key only together with NodeID and CoreGeneration.
	LogID   uint32 `json:"log_id"`
	Network string `json:"network,omitempty"` // tcp | udp
	SrcIP   string `json:"src_ip,omitempty"`
	SrcPort int    `json:"src_port,omitempty"`
	DstHost string `json:"dst_host,omitempty"` // as logged: hostname or ip
	DstIP   string `json:"dst_ip,omitempty"`   // resolved, when DNS lines were seen
	DstPort int    `json:"dst_port,omitempty"`

	// What the router decided.
	SniffedProtocol string `json:"sniffed_protocol,omitempty"`
	SniffedDomain   string `json:"sniffed_domain,omitempty"`
	RuleIndex       int    `json:"rule_index,omitempty"`
	RuleText        string `json:"rule_text,omitempty"`
	OutboundTag     string `json:"outbound_tag,omitempty"`
	OutboundType    string `json:"outbound_type,omitempty"`
	ChainEdgeUUID   string `json:"chain_edge_uuid,omitempty"`

	// Lifecycle.
	StartedAt time.Time `json:"started_at"`
	EndedAt   time.Time `json:"ended_at,omitzero"`
	// DurationMS comes from sing-box's own elapsed counter on the final line,
	// which is more trustworthy than subtracting two agent-side timestamps.
	DurationMS int64 `json:"duration_ms,omitempty"`
	// Open marks a periodic snapshot of a still-running connection. Snapshots
	// replace each other; only the final record has Open false.
	Open bool `json:"open,omitempty"`

	// Bytes, and whether they were ever actually measured. A short connection
	// dies between /connections samples and is never counted: BytesKnown false
	// means "we did not see", not "zero".
	Upload     int64 `json:"upload,omitempty"`
	Download   int64 `json:"download,omitempty"`
	BytesKnown bool  `json:"bytes_known,omitempty"`

	// How it ended, and whether it looked stuck. StalledAt is set when a
	// connection older than the stall floor went quiet in both directions;
	// cleared if it resumes. sing-box says nothing about a stalled TCP stream,
	// so this heuristic is the only signal there is.
	CloseReason string    `json:"close_reason,omitempty"`
	CloseError  string    `json:"close_error,omitempty"`
	StalledAt   time.Time `json:"stalled_at,omitzero"`

	// CoreGeneration changes when sing-box restarts. Records from a previous
	// generation that never closed are swept to CloseCoreRestart, and the count
	// is the blast radius of that restart.
	CoreGeneration uint64 `json:"core_generation,omitempty"`

	// SessionIDs are the trace sessions that captured this connection.
	SessionIDs []string `json:"session_ids,omitempty"`
	// HopPathID groups records stitched across machines.
	HopPathID string `json:"hop_path_id,omitempty"`
}

ConnRecord is one sing-box connection as assembled on the node. It is the central object of the whole feature: the row an operator filters, sorts, and opens.

type ConnRecordKey added in v0.2.19

type ConnRecordKey struct {
	NodeID         string    `json:"node_id"`
	CoreGeneration uint64    `json:"core_generation"`
	LogID          uint32    `json:"log_id"`
	StartedAt      time.Time `json:"started_at,omitzero"`
}

ConnRecordKey identifies one ConnRecord.

StartedAt is part of the identity, not decoration. sing-box's log id is rand.Uint32, so one core generation on one node can reuse it; the assembler deliberately splits those into two connections and the store's primary key keeps both. A key without StartedAt collapses them again wherever it is used, so a lookup returns whichever the query happened to order first and a hop view can walk into the wrong connection entirely.

func KeyOf added in v0.2.19

func KeyOf(r ConnRecord) ConnRecordKey

KeyOf builds the full identity of a record.

type DDNSProfile added in v0.2.0

type DDNSProfile struct {
	ID         string   `json:"id"`
	Name       string   `json:"name"`
	NodeID     string   `json:"node_id"`
	Provider   string   `json:"provider"`
	Domains    []string `json:"domains"`
	EnableIPv4 bool     `json:"enable_ipv4"`
	EnableIPv6 bool     `json:"enable_ipv6"`
	MaxRetries int      `json:"max_retries"`
	TTL        int      `json:"ttl"`
	// IntervalSeconds is how long the server waits between attempts for this
	// profile. A residential address wants minutes; a datacenter one that has
	// held the same IP for a year does not, and the interval is also what
	// spaces out retries when a provider is rejecting the write. Zero means the
	// server default.
	IntervalSeconds int `json:"interval_seconds,omitempty"`

	// Cloudflare provider
	CFAPIToken string `json:"cf_api_token,omitempty"`

	// Webhook provider. Body/URL support the templates #ip#, #domain#, #type#.
	WebhookURL     string `json:"webhook_url,omitempty"`
	WebhookMethod  string `json:"webhook_method,omitempty"`
	WebhookBody    string `json:"webhook_body,omitempty"`
	WebhookHeaders string `json:"webhook_headers,omitempty"`

	// Status (updated by the server after each run).
	LastIPv4  string    `json:"last_ipv4,omitempty"`
	LastIPv6  string    `json:"last_ipv6,omitempty"`
	LastRunAt time.Time `json:"last_run_at,omitempty"`
	LastError string    `json:"last_error,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

DDNSProfile describes how a node's public IP should be published to DNS. It is bound to a node; when that node's observed public IP changes, the bound profiles' records are updated.

type DNSDeployment added in v0.2.0

type DNSDeployment struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	NodeID string `json:"node_id"`
	Engine string `json:"engine"`

	ListenPort int    `json:"listen_port"`
	EnableUDP  bool   `json:"enable_udp"`
	EnableTCP  bool   `json:"enable_tcp"`
	Exposure   string `json:"exposure"`

	Zones []DNSZone `json:"zones"`

	Hostname      string `json:"hostname,omitempty"`
	PublishIPv4   bool   `json:"publish_ipv4"`
	PublishIPv6   bool   `json:"publish_ipv6"`
	RecordTTL     int    `json:"record_ttl,omitempty"`
	CFAPIToken    string `json:"cf_api_token,omitempty"`
	DDNSProfileID string `json:"ddns_profile_id,omitempty"`

	Status           string    `json:"status"`
	EngineVersion    string    `json:"engine_version,omitempty"`
	LastIPv4         string    `json:"last_ipv4,omitempty"`
	LastIPv6         string    `json:"last_ipv6,omitempty"`
	LastAppliedAt    time.Time `json:"last_applied_at,omitempty"`
	LastError        string    `json:"last_error,omitempty"`
	LastPublishedAt  time.Time `json:"last_published_at,omitempty"`
	LastPublishError string    `json:"last_publish_error,omitempty"`
	Disabled         bool      `json:"disabled,omitempty"`
	CreatedAt        time.Time `json:"created_at"`
	UpdatedAt        time.Time `json:"updated_at"`
}

DNSDeployment is the control-plane intent for a self-hosted DNS service on a node. CFAPIToken is a server-side secret and must never appear in read views or agent payloads.

type DNSRecord added in v0.2.0

type DNSRecord struct {
	Name  string `json:"name"`
	Type  string `json:"type"`
	Value string `json:"value"`
	TTL   int    `json:"ttl,omitempty"`
}

DNSRecord is a static authoritative record for a DNSZoneStatic zone.

type DNSZone added in v0.2.0

type DNSZone struct {
	Suffix    string      `json:"suffix"`
	Mode      string      `json:"mode"`
	Upstreams []string    `json:"upstreams,omitempty"`
	Records   []DNSRecord `json:"records,omitempty"`
}

DNSZone is one served block in a self-hosted resolver configuration. It is server-owned intent; the agent only receives the rendered, approved artifact.

type GeoRouting added in v0.2.0

type GeoRouting struct {
	ID          string   `json:"id"`
	Name        string   `json:"name"`
	Hostname    string   `json:"hostname"`     // the geo apex, e.g. dns.roobli.org
	NodeIDs     []string `json:"node_ids"`     // participating targets (need NodeGeo + IP)
	DNSNodeIDs  []string `json:"dns_node_ids"` // authoritative DNS nodes (run self-host DNS)
	TTL         int      `json:"ttl,omitempty"`
	Strategy    string   `json:"strategy"`                // "geoip" | "all-healthy"
	GeoIPDBPath string   `json:"geoip_db_path,omitempty"` // GeoLite2 path on the node

	// Parent-zone NS delegation reuses the referenced DDNSProfile's CF token.
	PublishNS     bool   `json:"publish_ns,omitempty"`
	DDNSProfileID string `json:"ddns_profile_id,omitempty"`

	LastRenderedSHA string    `json:"last_rendered_sha,omitempty"`
	Status          string    `json:"status,omitempty"`
	LastAppliedAt   time.Time `json:"last_applied_at,omitempty"`
	LastDelegatedAt time.Time `json:"last_delegated_at,omitempty"`
	LastError       string    `json:"last_error,omitempty"`
	CreatedAt       time.Time `json:"created_at"`
	UpdatedAt       time.Time `json:"updated_at"`
}

GeoRouting answers one apex hostname (e.g. dns.roobli.org) with the nearest healthy participating node, served by Lattice's own DNS nodes (Design 06, Path B). It carries no secrets: the NS-delegation token is reused from the referenced DDNSProfile.

type Group added in v0.2.4

type Group struct {
	ID          string         `json:"id"`   // "grp_<ulid>"
	Name        string         `json:"name"` // unique, display
	Slug        string         `json:"slug"` // url/nft-safe, unique, immutable
	Description string         `json:"description,omitempty"`
	Color       string         `json:"color"`               // design-token name, never raw hex (CSP)
	Icon        string         `json:"icon,omitempty"`      // lucide icon name
	ParentID    string         `json:"parent_id,omitempty"` // single parent; "" = root
	Order       int            `json:"order"`               // sort weight within the parent
	Members     []string       `json:"members"`             // explicit operator-pinned node IDs
	Selector    *GroupSelector `json:"selector,omitempty"`  // dynamic smart-membership source
	// LeaderID is the operator-designated group leader. It must be an explicit
	// Member of the group (validated on upsert); empty means "no leader". This is
	// the real, first-class field that replaces the old role-name heuristic used
	// by the dashboard to mark a node as its group's leader.
	LeaderID  string    `json:"leader_id,omitempty"`
	System    bool      `json:"system,omitempty"` // built-in (e.g. "Ungrouped"); limited edits
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Group is a first-class fleet organization entity. Members are explicit, operator-pinned node IDs; Selector is a dynamic smart-membership source used by dashboard grouping and group policy planning. Because selector matches can change when node tags, role, or geo facts change, group-policy plans must surface selector impact and require fresh approval. Color is a design-token name (e.g. "sky", "violet"), never a raw hex value, so the dashboard stays CSP-safe. Slug is url/nft-safe, unique, and treated as immutable once assigned. ParentID gives a single-parent hierarchy ("" = root); Order is the operator-controlled sort weight within a parent.

type GroupNetPolicy added in v0.2.4

type GroupNetPolicy struct {
	ID           string         `json:"id"`             // "gnp_<ulid>"
	ScopeGroupID string         `json:"scope_group_id"` // applies to members of this group
	Rules        []GroupNetRule `json:"rules"`
	Enabled      bool           `json:"enabled"`
	Priority     int            `json:"priority"` // lower wins when a node is in 2+ scoped groups
	CreatedAt    time.Time      `json:"created_at"`
	UpdatedAt    time.Time      `json:"updated_at"`
}

GroupNetPolicy is a group-scoped authoring layer over the unchanged per-node NetPolicy engine. The server expands it into one NetPolicy per member of ScopeGroupID before compilation; the agent never receives it. Priority breaks ties when a node is a member of two or more scoped groups (lower wins).

type GroupNetRule added in v0.2.4

type GroupNetRule struct {
	ID        string      `json:"id"`
	Comment   string      `json:"comment,omitempty"`
	Action    string      `json:"action"`
	Direction string      `json:"direction"`
	Protocol  string      `json:"protocol"`
	Ports     []int       `json:"ports,omitempty"`
	Remote    NetEndpoint `json:"remote"` // Kind may be NetRefGroup in addition to node|cidr|domain|any
	Disabled  bool        `json:"disabled,omitempty"`
}

GroupNetRule mirrors NetRule but its Remote may additionally be a group ref (Remote.Kind == NetRefGroup with Remote.GroupID set). The server fans a group remote out to one node-ref rule per resolved remote member during expansion, so the compiled per-node rule set only ever contains node/cidr/domain/any remotes.

type GroupSelector added in v0.2.4

type GroupSelector struct {
	MatchTagsAny   []string `json:"match_tags_any,omitempty"`
	MatchRoles     []string `json:"match_roles,omitempty"`
	MatchCountry   []string `json:"match_country,omitempty"`   // ISO-3166 alpha-2 codes
	MatchContinent []string `json:"match_continent,omitempty"` // AS/EU/NA/SA/AF/OC/AN
}

GroupSelector is a dynamic "smart group" membership rule. Dashboard grouping uses it for previews, and group-policy planning uses the current match set when expanding policies into per-node nft plans. Each field is an OR-set; a node matches the selector when it satisfies any one of the populated criteria (tags-any / roles / country / continent).

type GuardInterface added in v0.2.17

type GuardInterface struct {
	Name      string   `json:"name"`
	Addresses []string `json:"addresses,omitempty"`
	Up        bool     `json:"up,omitempty"`
}

GuardInterface is one network interface from a node reality report.

type GuardListener added in v0.2.17

type GuardListener struct {
	Protocol string `json:"protocol"` // tcp | udp
	Port     int    `json:"port"`
	Address  string `json:"address,omitempty"`
	Process  string `json:"process,omitempty"`
}

GuardListener is one listening socket from a node reality report.

type GuardNodeReality added in v0.2.17

type GuardNodeReality struct {
	NodeID        string           `json:"node_id"`
	Listeners     []GuardListener  `json:"listeners,omitempty"`
	Interfaces    []GuardInterface `json:"interfaces,omitempty"`
	ManagedSHA    string           `json:"managed_sha,omitempty"`
	ForeignTables []string         `json:"foreign_tables,omitempty"`
	NFTVersion    string           `json:"nft_version,omitempty"`
	CollectedAt   time.Time        `json:"collected_at"`
}

GuardNodeReality is the agent-reported ground truth for reality-first authoring and drift detection (design-13 §4.3). It is low-trust input: it feeds suggestions, diffs, and display only — never silent policy.

type GuardPortRange added in v0.2.17

type GuardPortRange struct {
	From int `json:"from"`
	To   int `json:"to"`
}

GuardPortRange is an inclusive L4 port range; From == To expresses a single port. Security-group rules use ranges instead of bare port lists so "9009-9013" stays one reviewable entry (design-13 §4.2).

type GuardRule added in v0.2.17

type GuardRule struct {
	ID        string           `json:"id"`
	Comment   string           `json:"comment,omitempty"`
	Action    string           `json:"action"`    // allow | deny
	Direction string           `json:"direction"` // ingress | egress
	Protocol  string           `json:"protocol"`  // tcp | udp | icmp | icmpv6 | any
	Ports     []GuardPortRange `json:"ports,omitempty"`
	Remote    NetEndpoint      `json:"remote"` // Kind may additionally be NetRefZone
	Log       bool             `json:"log,omitempty"`
	Disabled  bool             `json:"disabled,omitempty"`
}

GuardRule is one ordered security-group rule. Empty Ports means all ports for the selected protocol. Log is an L2 render surface (design-13 §4.6): the field is part of the authoring contract now, but the compiler rejects it until renderer support lands.

type GuardZone added in v0.2.17

type GuardZone struct {
	ID          string    `json:"id"`
	Name        string    `json:"name"`
	Builtin     bool      `json:"builtin,omitempty"`
	Interfaces  []string  `json:"interfaces,omitempty"`
	CIDRs       []string  `json:"cidrs,omitempty"`
	Description string    `json:"description,omitempty"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
}

GuardZone is a named trust surface built from interface names and/or CIDRs. Builtin zones carry well-known ids (GuardZone* constants) and resolve per-node facts (public interface, overlay membership) at compile/view time; operator zones pin explicit interfaces/CIDRs. Zone references appear both as a rule remote (match scope) and as a node binding's trusted-zone list (full accept), reviewed like every other plan input. (design-13 §4.1)

type HopPath added in v0.2.19

type HopPath struct {
	ID         string `json:"id"`
	Confidence string `json:"confidence"`
	// RecordKeys are (node_id, core_generation, log_id) triples in hop order.
	RecordKeys []ConnRecordKey `json:"record_keys"`
	// Candidates is populated only when Confidence is ambiguous, so the operator
	// sees what the stitcher could not choose between rather than a guess.
	Candidates []ConnRecordKey `json:"candidates,omitempty"`
}

HopPath is a connection stitched across machines.

type HostFacts added in v0.2.0

type HostFacts struct {
	Hostname        string    `json:"hostname,omitempty"`
	OS              string    `json:"os,omitempty"`
	Platform        string    `json:"platform,omitempty"`
	PlatformVersion string    `json:"platform_version,omitempty"`
	KernelVersion   string    `json:"kernel_version,omitempty"`
	Arch            string    `json:"arch,omitempty"`
	CPUCores        int       `json:"cpu_cores,omitempty"`
	CPUModel        string    `json:"cpu_model,omitempty"`
	MemoryTotal     uint64    `json:"memory_total,omitempty"`
	SwapTotal       uint64    `json:"swap_total,omitempty"`
	Virtualization  string    `json:"virtualization,omitempty"`
	BootTime        time.Time `json:"boot_time,omitempty"`
	ReportedAt      time.Time `json:"reported_at,omitempty"`
}

HostFacts are auto-detected, slow-changing machine facts reported by the node-agent. They are advisory low-trust telemetry: useful for display, inventory and map planning, but never for authorization decisions.

type KVEntry

type KVEntry struct {
	Bucket    string    `json:"bucket"`
	Key       string    `json:"key"`
	Value     string    `json:"value"`
	UpdatedAt time.Time `json:"updated_at"`
}

type LogBatch added in v0.2.0

type LogBatch struct {
	SourceID   string    `json:"source_id"`
	Path       string    `json:"path"`      // echoed for server cross-check vs the source record
	RotID      string    `json:"rot_id"`    // opaque per-file-incarnation id (inode/ctime)
	FirstOff   uint64    `json:"first_off"` // offset before the first line in this batch
	LastOff    uint64    `json:"last_off"`  // offset after the last line (== agent checkpoint)
	Dropped    uint64    `json:"dropped"`   // lines the agent dropped (backpressure) since last batch
	Lines      []string  `json:"lines"`     // raw lines, ordered, no trailing newline
	CapturedAt time.Time `json:"captured_at"`
}

LogBatch is the agent -> server ingest envelope (one source per batch).

type LogLine added in v0.2.0

type LogLine struct {
	SourceID  string    `json:"source_id"`
	NodeID    string    `json:"node_id"`
	Path      string    `json:"path"`
	Seq       uint64    `json:"seq"`
	Offset    uint64    `json:"offset"`
	At        time.Time `json:"at"`
	Line      string    `json:"line"`
	Truncated bool      `json:"truncated,omitempty"`
}

LogLine is one ingested line as persisted/queried. Seq is the server-assigned monotonic per-source ingest sequence (the query cursor); Offset is the agent's byte offset after this line in the (rotation-scoped) source file.

type LogSource added in v0.2.0

type LogSource struct {
	ID            string    `json:"id"`
	Name          string    `json:"name"`
	NodeID        string    `json:"node_id"`
	Path          string    `json:"path"`
	Enabled       bool      `json:"enabled"`
	MaxLineBytes  int       `json:"max_line_bytes"`  // truncate longer lines (server default 16384)
	MaxBatchLines int       `json:"max_batch_lines"` // agent batch cap (server default 500)
	CreatedAt     time.Time `json:"created_at"`
	UpdatedAt     time.Time `json:"updated_at"`
}

LogSource declares a file on a node whose appended lines are tailed by the assigned agent and shipped to the server. It is assignment-driven like Monitor: exactly one node owns a source (a path is node-local), identified by NodeID. LogSource carries no secrets.

type MachineProfile added in v0.2.0

type MachineProfile struct {
	ID     string `json:"id"`
	NodeID string `json:"node_id"`
	Label  string `json:"label,omitempty"`

	Vendor     string `json:"vendor,omitempty"`
	ConsoleURL string `json:"console_url,omitempty"`
	DetailURL  string `json:"detail_url,omitempty"`
	Region     string `json:"region,omitempty"`
	Notes      string `json:"notes,omitempty"`

	PriceCents int64  `json:"price_cents,omitempty"`
	Currency   string `json:"currency,omitempty"`

	PurchasedAt  time.Time `json:"purchased_at,omitempty"`
	RenewalCycle string    `json:"renewal_cycle,omitempty"`
	CycleDays    int       `json:"cycle_days,omitempty"`
	NextRenewal  time.Time `json:"next_renewal,omitempty"`
	AutoRoll     bool      `json:"auto_roll"`

	RemindDaysBefore []int  `json:"remind_days_before,omitempty"`
	RemindersEnabled bool   `json:"reminders_enabled"`
	LastRemindedKey  string `json:"last_reminded_key,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

MachineProfile is operator-authored inventory, cost, and renewal metadata for a node. It is server-only control-plane state: it must never be sent to an agent or used by an agent. ConsoleURL and DetailURL may carry account-specific or signed links and are encrypted at rest by lattice-server.

type MachineVendor added in v0.2.17

type MachineVendor struct {
	ID          string    `json:"id"`
	Name        string    `json:"name"`
	URL         string    `json:"url,omitempty"`
	LogoURL     string    `json:"logo_url,omitempty"`
	Description string    `json:"description,omitempty"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
}

MachineVendor is operator-authored metadata for a provider/vendor referenced by machine profiles. MachineProfile.Vendor remains the compatibility key; the server matches this record by Name so old profiles do not require migration.

type Metrics

type Metrics struct {
	CPUPercent  float64 `json:"cpu_percent"`
	Load1       float64 `json:"load1"`
	Load5       float64 `json:"load5"`
	Load15      float64 `json:"load15"`
	MemoryUsed  uint64  `json:"memory_used"`
	MemoryTotal uint64  `json:"memory_total"`
	DiskUsed    uint64  `json:"disk_used"`
	DiskTotal   uint64  `json:"disk_total"`
	NetRxBytes  uint64  `json:"net_rx_bytes"`
	NetTxBytes  uint64  `json:"net_tx_bytes"`
	// NetRxSpeed / NetTxSpeed are bytes-per-second rates the agent derives from
	// the delta of the cumulative byte counters between two metrics cycles. The
	// first cycle after agent start (no prior sample) and any counter reset both
	// report 0. Dashboards read these for live bandwidth; the cumulative
	// NetRxBytes/NetTxBytes remain the source of truth.
	NetRxSpeed    float64   `json:"net_rx_speed"`
	NetTxSpeed    float64   `json:"net_tx_speed"`
	UptimeSeconds uint64    `json:"uptime_seconds"`
	CollectedAt   time.Time `json:"collected_at"`
}

type Monitor added in v0.2.0

type Monitor struct {
	ID          string    `json:"id"`
	Name        string    `json:"name"`
	Type        string    `json:"type"`
	Target      string    `json:"target"`
	IntervalSec int       `json:"interval_sec"`
	TimeoutSec  int       `json:"timeout_sec"`
	AssignAll   bool      `json:"assign_all"`
	NodeIDs     []string  `json:"node_ids,omitempty"`
	Enabled     bool      `json:"enabled"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
}

Monitor is a periodic reachability/latency probe executed by assigned agents. Targets are host:port for tcp/icmp probes and a URL for http probes. A monitor runs on every node when AssignAll is set, otherwise on the nodes in NodeIDs — this is how a group's members continuously measure their group leader.

type MonitorResult added in v0.2.0

type MonitorResult struct {
	MonitorID string    `json:"monitor_id"`
	NodeID    string    `json:"node_id"`
	At        time.Time `json:"at"`
	Success   bool      `json:"success"`
	LatencyMs float64   `json:"latency_ms"`
	Error     string    `json:"error,omitempty"`
}

MonitorResult is a single probe outcome reported by an agent.

type NFTInputs added in v0.2.0

type NFTInputs struct {
	ID     string `json:"id"`
	NodeID string `json:"node_id"`

	InterfaceName string `json:"interface_name,omitempty"`
	WireGuardCIDR string `json:"wireguard_cidr,omitempty"`

	PublicTCP    []int `json:"public_tcp,omitempty"`
	PublicUDP    []int `json:"public_udp,omitempty"`
	WireGuardTCP []int `json:"wireguard_tcp,omitempty"`
	WireGuardUDP []int `json:"wireguard_udp,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

NFTInputs are the authoritative, server-owned baseline nftables inputs for a single node. Other core providers (DNS, per-node ACL, proxy cores) compose their required ports/rules into this shape before rendering the one lattice_guard table; they must not create competing nft tables.

type NetEndpoint added in v0.2.0

type NetEndpoint struct {
	Kind    string `json:"kind"`
	NodeID  string `json:"node_id,omitempty"`
	CIDR    string `json:"cidr,omitempty"`
	Domain  string `json:"domain,omitempty"`
	GroupID string `json:"group_id,omitempty"` // set when Kind == NetRefGroup; resolved to node refs before compile
	ZoneID  string `json:"zone_id,omitempty"`  // set when Kind == NetRefZone; resolved to iface/CIDR matches before compile
}

NetEndpoint describes the non-target side of a policy rule. Node refs are resolved by the server at validation/graph/compile time. Domain refs are egress-only and compile to named nft sets that the node refreshes through the agent's DNS updater; they are not accepted as ingress identity.

type NetPolicy added in v0.2.0

type NetPolicy struct {
	ID           string    `json:"id"`
	TargetNodeID string    `json:"target_node_id"`
	Rules        []NetRule `json:"rules"`
	Enabled      bool      `json:"enabled"`
	// GroupDerived marks a per-node policy materialized from one or more
	// GroupNetPolicy documents (server-side expansion). Manually-authored
	// per-node policies leave it false. Materialization refuses to overwrite a
	// manual policy, so the two authoring lanes never silently clobber.
	GroupDerived  bool      `json:"group_derived,omitempty"`
	LastPlanSHA   string    `json:"last_plan_sha,omitempty"`
	LastAppliedAt time.Time `json:"last_applied_at,omitempty"`
	LastError     string    `json:"last_error,omitempty"`
	CreatedAt     time.Time `json:"created_at"`
	UpdatedAt     time.Time `json:"updated_at"`
}

NetPolicy is the per-node network intent document. It is control-plane state only: the agent does not receive it directly. A later iteration compiles this policy into nft with dead-man rollback.

type NetRule added in v0.2.0

type NetRule struct {
	ID        string      `json:"id"`
	Comment   string      `json:"comment,omitempty"`
	Action    string      `json:"action"`
	Direction string      `json:"direction"`
	Protocol  string      `json:"protocol"`
	Ports     []int       `json:"ports,omitempty"`
	Remote    NetEndpoint `json:"remote"`
	Disabled  bool        `json:"disabled,omitempty"`
}

NetRule is an ordered operator-authored L3/L4 policy rule evaluated on the target node. Empty Ports means all ports for the selected protocol.

type Node

type Node struct {
	ID                  string   `json:"id"`
	LatticeIdentityUUID string   `json:"lattice_identity_uuid,omitempty"`
	Name                string   `json:"name"`
	Comment             string   `json:"comment,omitempty"`
	TokenHash           string   `json:"token_hash"`
	Tags                []string `json:"tags"`
	Role                string   `json:"role"`
	// Inventory is operator-registered provenance/quality metadata (dashboard
	// fleet inventory). Operator-owned; agents never report or overwrite it.
	Inventory          *NodeInventory `json:"inventory,omitempty"`
	WireGuardIP        string         `json:"wireguard_ip"`
	WireGuardPublicKey string         `json:"wireguard_public_key,omitempty"`
	WireGuardEndpoint  string         `json:"wireguard_endpoint,omitempty"`
	WireGuardPort      int            `json:"wireguard_port,omitempty"`
	PublicIP           string         `json:"public_ip"`
	PublicIPv6         string         `json:"public_ipv6,omitempty"`
	// InternalIP / InternalIPv6 are the node's LAN/primary-interface addresses,
	// reported by the agent. Informational (not geocoded); private ranges allowed.
	InternalIP   string `json:"internal_ip,omitempty"`
	InternalIPv6 string `json:"internal_ipv6,omitempty"`
	AgentVersion string `json:"agent_version"`
	Online       bool   `json:"online"`
	Disabled     bool   `json:"disabled,omitempty"`
	// AgentSourceAllowlist optionally restricts node-agent bearer-token use to
	// exact source IPs or CIDR prefixes as resolved by the server's client-IP
	// trust policy. Empty means no source-address restriction.
	AgentSourceAllowlist []string `json:"agent_source_allowlist,omitempty"`
	// TokenLastUsedAt is coarse lifecycle telemetry for the node credential. It
	// is updated only after successful bearer-token authentication and may be
	// write-throttled by the server to avoid heartbeat write amplification.
	TokenLastUsedAt time.Time        `json:"token_last_used_at,omitempty"`
	LastSeen        time.Time        `json:"last_seen"`
	Metrics         Metrics          `json:"metrics"`
	HostFacts       HostFacts        `json:"host_facts"`
	Geo             *NodeGeo         `json:"geo,omitempty"`
	AgentDebug      AgentDebugPolicy `json:"agent_debug"`
	// Trace is this node's sing-box trace collection policy. It sits beside
	// AgentDebug rather than in its own map because it is node behaviour, not a
	// separate resource: it is audited but never goes through the approval
	// chain, since it changes what the agent subscribes to and never touches
	// the node's own configuration.
	Trace TracePolicy `json:"trace"`
	// AgentLaunch is the last operator-authored installer/startup profile used
	// to generate an enrollment or reconfigure command. It is advisory desired
	// state, not proof of the currently running process flags.
	AgentLaunch *AgentLaunchConfig `json:"agent_launch,omitempty"`
	// TerminalTransport is the operator-owned per-node terminal transport: "poll"
	// (default) or "stream". Empty is treated as the deployment default. It is the
	// rollout lever for promoting the streaming terminal one node at a time; the
	// agent reads it from its polled AgentConfig and applies it to new sessions.
	TerminalTransport string `json:"terminal_transport,omitempty"`
	// IPConfig is the operator-owned, per-node override for how the agent
	// discovers its public IPs (mirrors the agent's -ip-mode/-ip-resolvers
	// flags). nil means "no override" — the agent keeps its startup flags. It is
	// pushed down through the polled AgentConfig.
	IPConfig *NodeIPConfig `json:"ip_config,omitempty"`
	// GroupIDs is the node's resolved group memberships. It is a server-computed,
	// read-only convenience field (the union of every group whose explicit
	// Members or display Selector resolves this node); it is never authored by a
	// client and is not persisted as node intent. Tags/Role remain the underlying
	// facts that selectors read.
	GroupIDs  []string  `json:"group_ids,omitempty"`
	CreatedAt time.Time `json:"created_at"`
}

type NodeGeo added in v0.2.0

type NodeGeo struct {
	Country   string    `json:"country,omitempty"`
	Region    string    `json:"region,omitempty"`
	City      string    `json:"city,omitempty"`
	Lat       float64   `json:"lat,omitempty"`
	Lon       float64   `json:"lon,omitempty"`
	IP        string    `json:"ip,omitempty"`
	ASN       int       `json:"asn,omitempty"`
	ASOrg     string    `json:"as_org,omitempty"`
	Provider  string    `json:"provider,omitempty"`
	Source    string    `json:"source,omitempty"`
	UpdatedAt time.Time `json:"updated_at,omitempty"`
}

NodeGeo is map metadata for a node. Operator-entered values are authoritative; automatic GeoIP values are advisory and should not overwrite operator values unless an operator explicitly asks for that replacement.

type NodeGuardBinding added in v0.2.17

type NodeGuardBinding struct {
	NodeID          string      `json:"node_id"`
	GroupIDs        []string    `json:"group_ids"`
	Overrides       []GuardRule `json:"overrides,omitempty"`
	ZoneIDs         []string    `json:"zone_ids,omitempty"`
	Managed         bool        `json:"managed"`
	Version         int64       `json:"version"`
	LastPlanSHA     string      `json:"last_plan_sha,omitempty"`
	LastAppliedAt   time.Time   `json:"last_applied_at,omitempty"`
	LastError       string      `json:"last_error,omitempty"`
	AppliedTableSHA string      `json:"applied_table_sha,omitempty"`
	CreatedAt       time.Time   `json:"created_at"`
	UpdatedAt       time.Time   `json:"updated_at"`
}

NodeGuardBinding composes a node's effective firewall: base scaffold, then trusted zones, then Overrides, then attached groups in GroupIDs order. Managed=false marks an observe-only node the compiler must refuse to plan. The Last*/AppliedTableSHA fields carry apply state and the drift anchor the legacy NFTInputs record never had (design-13 §4.2).

type NodeIPConfig added in v0.2.5

type NodeIPConfig struct {
	Mode         string    `json:"mode,omitempty"` // "" | auto | static | resolver | script
	StaticIPv4   string    `json:"static_ipv4,omitempty"`
	StaticIPv6   string    `json:"static_ipv6,omitempty"`
	Resolvers    []string  `json:"resolvers,omitempty"` // IP-echo URLs; empty = agent defaults
	Script       string    `json:"script,omitempty"`    // server->agent only; redact from node views
	ScriptSHA256 string    `json:"script_sha256,omitempty"`
	UpdatedAt    time.Time `json:"updated_at,omitempty"`
}

NodeIPConfig is the operator-owned, per-node override for how the agent determines its public IPs. It mirrors the agent's -ip-mode / -ip-resolvers / -public-ip startup flags so the server can push a change without a redeploy. An empty Mode means "no override". Script discovery is high-trust node-local code: the server stores Script for the agent only, and read-facing node views should redact Script while keeping ScriptSHA256 for operator confirmation.

type NodeInventory added in v0.2.15

type NodeInventory struct {
	// PurityPercent is the operator-assessed IP purity score, 0-100.
	PurityPercent *int `json:"purity_percent,omitempty"`
	// Quality is a coarse grade such as "high", "medium", "low" (free-form, <=64 chars).
	Quality string `json:"quality,omitempty"`
	// Notes is free-form inventory context (<=2048 chars, matching MachineProfile.Notes).
	Notes string `json:"notes,omitempty"`
}

NodeInventory carries the operator's own assessment of a node's provenance quality — e.g. "98% pure, high quality" residential/ISP purity notes kept in the fleet inventory. Purely informational; nothing in the control plane branches on it.

type NotifyChannel added in v0.2.0

type NotifyChannel struct {
	ID        string            `json:"id"`
	Name      string            `json:"name"`
	Kind      string            `json:"kind"`
	Config    map[string]string `json:"config,omitempty"`
	Enabled   bool              `json:"enabled"`
	CreatedAt time.Time         `json:"created_at"`
	UpdatedAt time.Time         `json:"updated_at"`
}

NotifyChannel is a persisted notification destination. Config holds provider-specific fields (e.g. token, chat_id, webhook_url); its values are secret and never serialized back to clients.

type NotifyRule added in v0.2.1

type NotifyRule struct {
	ID            string    `json:"id"`
	Name          string    `json:"name"`
	EventTypes    []string  `json:"event_types,omitempty"`
	ChannelIDs    []string  `json:"channel_ids,omitempty"`
	TitleTemplate string    `json:"title_template,omitempty"`
	BodyTemplate  string    `json:"body_template,omitempty"`
	Enabled       bool      `json:"enabled"`
	CreatedAt     time.Time `json:"created_at"`
	UpdatedAt     time.Time `json:"updated_at"`
}

NotifyRule routes notification events to one or more destinations. EventTypes uses stable server event ids such as monitor.down or ssh.login; "*" matches all notification events. Templates are intentionally small string templates expanded by the server with event_type, title, and body variables.

type OIDCIdentity added in v0.2.0

type OIDCIdentity struct {
	ProviderID string    `json:"provider_id"`
	Issuer     string    `json:"issuer"`
	Subject    string    `json:"subject"`
	UserID     string    `json:"user_id"`
	Email      string    `json:"email,omitempty"`
	CreatedAt  time.Time `json:"created_at"`
}

OIDCIdentity is the durable link between an external subject and a local user. Keyed in the store by (ProviderID, Subject): the trust anchor is the admin-vetted provider record, not the bare issuer string, so a second provider that happens to share an issuer cannot reuse another provider's links. Subject is the stable identifier; Email/Issuer are reference only.

type OIDCProvider added in v0.2.0

type OIDCProvider struct {
	ID             string    `json:"id"`
	DisplayName    string    `json:"display_name"`
	Issuer         string    `json:"issuer"`
	ClientID       string    `json:"client_id"`
	ClientSecret   string    `json:"client_secret,omitempty"`
	Scopes         []string  `json:"scopes,omitempty"`
	AllowedDomains []string  `json:"allowed_domains,omitempty"`
	Enabled        bool      `json:"enabled"`
	CreatedAt      time.Time `json:"created_at"`
	UpdatedAt      time.Time `json:"updated_at"`
}

OIDCProvider is an admin-configured external identity provider for SSO login. ClientSecret is a secret-at-rest field (encrypted by the store boundary) and is never returned by the API.

type PluginInstallation added in v0.2.0

type PluginInstallation struct {
	ID             string    `json:"id"`
	Name           string    `json:"name"`
	Type           string    `json:"type"`
	Version        string    `json:"version,omitempty"`
	Entrypoint     string    `json:"entrypoint,omitempty"`
	Publisher      string    `json:"publisher,omitempty"`
	Capabilities   []string  `json:"capabilities"`
	ArtifactSHA256 string    `json:"artifact_sha256,omitempty"`
	BundlePath     string    `json:"bundle_path,omitempty"`
	Status         string    `json:"status"`
	VerifiedAt     time.Time `json:"verified_at,omitempty"`
	InstalledAt    time.Time `json:"installed_at,omitempty"`
	ActivatedAt    time.Time `json:"activated_at,omitempty"`
	DisabledAt     time.Time `json:"disabled_at,omitempty"`
	CreatedAt      time.Time `json:"created_at"`
	UpdatedAt      time.Time `json:"updated_at"`
}

PluginInstallation is the persisted lifecycle record for a verified plugin bundle. It is intentionally metadata-only: artifacts and runtime handles stay outside the shared API model.

type ProxyInbound added in v0.2.0

type ProxyInbound struct {
	ID       string `json:"id"`
	Name     string `json:"name"`
	Core     string `json:"core"`
	Protocol string `json:"protocol"`
	Listen   string `json:"listen,omitempty"`
	Port     int    `json:"port"`

	Transport string `json:"transport,omitempty"`
	Path      string `json:"path,omitempty"`
	Host      string `json:"host,omitempty"`

	Security    string   `json:"security,omitempty"`
	SNI         string   `json:"sni,omitempty"`
	ALPN        []string `json:"alpn,omitempty"`
	Fingerprint string   `json:"fingerprint,omitempty"`

	CertPath string `json:"cert_path,omitempty"`
	KeyPath  string `json:"key_path,omitempty"`

	RealityPrivateKey string   `json:"reality_private_key,omitempty"`
	RealityPublicKey  string   `json:"reality_public_key,omitempty"`
	RealityShortIDs   []string `json:"reality_short_ids,omitempty"`
	RealityDest       string   `json:"reality_dest,omitempty"`

	SSMethod string `json:"ss_method,omitempty"`

	Enabled   bool      `json:"enabled"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

ProxyInbound is the central protocol/transport template that the server renders into node-local sing-box/xray config. RealityPrivateKey is a server-side secret and must never appear in API views or agent payloads.

type ProxyNodeProfile added in v0.2.0

type ProxyNodeProfile struct {
	ID         string   `json:"id"`
	NodeID     string   `json:"node_id"`
	Core       string   `json:"core"`
	InboundIDs []string `json:"inbound_ids"`
	Hostname   string   `json:"hostname,omitempty"`
	ListenIP   string   `json:"listen_ip,omitempty"`

	ConfigPath string `json:"config_path,omitempty"`
	StatsAPI   string `json:"stats_api,omitempty"`

	// LogLevel is the sing-box log.level rendered into this node's config. It
	// governs only what lands on disk. The trace subscription level is separate
	// and independent (model.TracePolicy): sing-box emits to Clash API
	// subscribers without applying log.level, which is what lets verbosity
	// change without a restart. Empty renders the default.
	LogLevel string `json:"log_level,omitempty"`

	// ClashAPI is the loopback address of this node's sing-box Clash API, for
	// example "127.0.0.1:9090". Empty means the API is not rendered. It must be
	// a loopback address: the renderer rejects anything else, because this
	// endpoint exposes live connection data and log lines.
	ClashAPI string `json:"clash_api,omitempty"`

	// ClashAPISecret is the bearer token for ClashAPI. It is rendered into the
	// node config, which already carries user UUIDs and REALITY private keys and
	// is handled as a node-scoped secret-bearing artifact. The agent reads it
	// back out of the applied config on the node; it is never shipped to the
	// agent by any other path.
	ClashAPISecret string `json:"clash_api_secret,omitempty"`

	AppliedSHA256 string    `json:"applied_sha256,omitempty"`
	LastApplyAt   time.Time `json:"last_apply_at,omitempty"`
	LastError     string    `json:"last_error,omitempty"`

	// Usage collector health is agent-reported and server persisted for
	// operator visibility. It is not client-editable policy and must not affect
	// the server's monotonic usage accounting.
	UsageCollectorSource      string    `json:"usage_collector_source,omitempty"`
	UsageCollectorStatus      string    `json:"usage_collector_status,omitempty"`
	UsageCollectorCheckedAt   time.Time `json:"usage_collector_checked_at,omitempty"`
	UsageCollectorLastOKAt    time.Time `json:"usage_collector_last_ok_at,omitempty"`
	UsageCollectorLastError   string    `json:"usage_collector_last_error,omitempty"`
	UsageCollectorLastErrorAt time.Time `json:"usage_collector_last_error_at,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

ProxyNodeProfile binds the central proxy model to a single node. One profile exists per node; it is the unit rendered into a reviewed plan/apply task.

type ProxyUsageSnapshot added in v0.2.0

type ProxyUsageSnapshot struct {
	NodeID        string           `json:"node_id"`
	At            time.Time        `json:"at"`
	CoreUptimeSec uint64           `json:"core_uptime_sec"`
	UserBytes     map[string]int64 `json:"user_bytes"`
	// LineUserBytes optionally carries cumulative counters split by stable
	// line_hash_id and proxy user id. It is additive to UserBytes so old
	// collectors remain valid; if a collector sends only line_user_bytes, agents
	// and servers may derive user_bytes by summing per-user line counters.
	LineUserBytes map[string]map[string]int64 `json:"line_user_bytes,omitempty"`

	// Collector fields describe this collection attempt. They let the agent
	// report local collector errors without overwriting the previous accounting
	// baseline on the server.
	CollectorSource    string    `json:"collector_source,omitempty"` // file | http | future core transport
	CollectorStatus    string    `json:"collector_status,omitempty"` // ok | error
	CollectorError     string    `json:"collector_error,omitempty"`
	CollectorCheckedAt time.Time `json:"collector_checked_at,omitempty"`
}

ProxyUsageSnapshot is the last node-reported accounting snapshot. The server later diffs successive snapshots to advance ProxyUser.UsedBytes monotonically.

type ProxyUser added in v0.2.0

type ProxyUser struct {
	ID      string `json:"id"`
	Name    string `json:"name"`
	Enabled bool   `json:"enabled"`

	UUID     string `json:"uuid,omitempty"`
	Password string `json:"password,omitempty"`
	SubToken string `json:"sub_token,omitempty"`

	InboundIDs        []string  `json:"inbound_ids,omitempty"`
	TrafficLimitBytes int64     `json:"traffic_limit_bytes,omitempty"`
	ExpiresAt         time.Time `json:"expires_at,omitempty"`

	UsedBytes  int64     `json:"used_bytes"`
	LastSeenAt time.Time `json:"last_seen_at,omitempty"`
	Status     string    `json:"status"`

	// Server-managed notification cursors. They prevent repeated quota/expiry
	// alerts after the operator has already been notified for the current limit
	// or expiry date.
	LastQuotaNotifiedKey  string `json:"last_quota_notified_key,omitempty"`
	LastExpiryNotifiedKey string `json:"last_expiry_notified_key,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

ProxyUser is a central subscriber identity. UUID, Password and SubToken are bearer credential material; they are encrypted at rest and only surfaced through one-time/admin rotation flows, never through list/read views.

type SecurityGroup added in v0.2.17

type SecurityGroup struct {
	ID          string      `json:"id"`
	Name        string      `json:"name"`
	Description string      `json:"description,omitempty"`
	Rules       []GuardRule `json:"rules"`
	Version     int64       `json:"version"`
	CreatedAt   time.Time   `json:"created_at"`
	UpdatedAt   time.Time   `json:"updated_at"`
}

SecurityGroup is a named, reusable, ordered rule set attachable to any number of nodes through NodeGuardBinding. Version implements optimistic concurrency on upserts so two operators cannot silently clobber each other (design-13 §4.2, closing the NFTInputs gap).

type ShareSource added in v0.2.19

type ShareSource struct {
	Kind           string `json:"kind"`
	PluginID       string `json:"plugin_id,omitempty"`
	SubscriptionID string `json:"subscription_id,omitempty"`
	ProxyUserID    string `json:"proxy_user_id,omitempty"`
}

ShareSource names where a share's content comes from. Exactly one kind is set, and the fields that do not belong to that kind stay empty.

type SingBoxInventory added in v0.2.7

type SingBoxInventory struct {
	NodeID      string        `json:"node_id"`
	At          time.Time     `json:"at"`
	CoreVersion string        `json:"core_version,omitempty"`
	Nodes       []SingBoxNode `json:"nodes"`
	// Network is how this node is reached: nat | direct. Declared by the node
	// itself, since whether a provider sits in front of it is not something the
	// control plane can infer from the outside.
	Network string `json:"network,omitempty"`
	// ProviderEdge is the hostname the outside world actually connects to when a
	// provider forwards into this node. It is not the node's own public host: a
	// relay elsewhere in the fleet points its outbound at the provider edge, so
	// without it a chain into a NAT node cannot be matched back to the line it
	// lands on. Declared by the node, for the same reason Network is.
	ProviderEdge string `json:"provider_edge,omitempty"`
	Status       string `json:"status,omitempty"` // ok | error
	Error        string `json:"error,omitempty"`
}

SingBoxInventory is the latest snapshot of the sing-box nodes discovered on one machine. It is reported by the agent (read-only `sb --json list`) and held in memory on the server as a live mirror — it is re-reported each interval and is not persisted (a restart simply waits for the next report). Status/Error let a node report a discovery failure (e.g. sb not installed) without a stale list.

type SingBoxNode added in v0.2.7

type SingBoxNode struct {
	Name             string `json:"name"`
	LineID           string `json:"line_id,omitempty"`
	LineUUID         string `json:"line_uuid,omitempty"` // design-15 D1: control-plane line identity from the sidecar
	NodeIdentityUUID string `json:"node_identity_uuid,omitempty"`
	Protocol         string `json:"protocol,omitempty"`
	Network          string `json:"network,omitempty"`
	Address          string `json:"address,omitempty"`
	Port             string `json:"port,omitempty"`
	SNI              string `json:"sni,omitempty"`
	Host             string `json:"host,omitempty"`
	ListenHost       string `json:"listen_host,omitempty"`
	// PublicPort is the port the outside world reaches this inbound on, when it
	// differs from the port sing-box listens on. A NAT node behind a provider
	// edge is the case that needs it: the box listens on 488 while the world
	// dials 50100. Empty means the listen port is also the public one.
	//
	// It comes from a node-owned endpoints file rather than from the running
	// config, because no amount of reading sing-box can reveal a mapping that
	// lives in someone else's router.
	PublicPort     string `json:"public_port,omitempty"`
	OutboundRef    string `json:"outbound_ref,omitempty"`
	OutboundServer string `json:"outbound_server,omitempty"`
	OutboundPort   string `json:"outbound_port,omitempty"`
	OutboundType   string `json:"outbound_type,omitempty"`
	// DownstreamLineUUID is the declared chain edge target (design-15 §6), read
	// from the sidecar chain block; empty when the line is single-exit or the
	// edge is only inferable from outbound host/port.
	DownstreamLineUUID string            `json:"downstream_line_uuid,omitempty"`
	UserCount          int               `json:"user_count,omitempty"`
	UserKnown          bool              `json:"user_known,omitempty"`
	Metadata           map[string]string `json:"metadata,omitempty"`
	PublicKey          string            `json:"public_key,omitempty"`
	ShareURL           string            `json:"share_url,omitempty"`
}

SingBoxNode is one inbound discovered on a node by reading its on-box sing-box management state (the 233boy `sb --json list` output). It is the adoption-model view of a proxy that exists on the machine but is NOT (necessarily) managed by Lattice's own proxy store — the bridge that lets the dashboard see proxies on machines provisioned out-of-band. Secret-free: share_url already encodes the connection without exposing additional server-side material.

type StaticObject

type StaticObject struct {
	Bucket      string    `json:"bucket"`
	Path        string    `json:"path"`
	Content     string    `json:"content"`
	ContentType string    `json:"content_type"`
	Size        int       `json:"size"`
	UpdatedAt   time.Time `json:"updated_at"`
}

type StorageAccessToken added in v0.2.1

type StorageAccessToken struct {
	ID         string    `json:"id"`
	Name       string    `json:"name"`
	TokenHash  string    `json:"token_hash,omitempty"`
	Kind       string    `json:"kind"`
	Access     string    `json:"access"`
	Buckets    []string  `json:"buckets,omitempty"`
	RevokedAt  time.Time `json:"revoked_at,omitempty"`
	LastUsedAt time.Time `json:"last_used_at,omitempty"`
	CreatedAt  time.Time `json:"created_at"`
	UpdatedAt  time.Time `json:"updated_at"`
}

type StorageBinding added in v0.2.1

type StorageBinding struct {
	ID         string    `json:"id"`
	Kind       string    `json:"kind"`
	Bucket     string    `json:"bucket"`
	Hostname   string    `json:"hostname"`
	PathPrefix string    `json:"path_prefix,omitempty"`
	Enabled    bool      `json:"enabled"`
	CreatedAt  time.Time `json:"created_at"`
	UpdatedAt  time.Time `json:"updated_at"`
}

type StorageBucket added in v0.2.1

type StorageBucket struct {
	ID               string    `json:"id"`
	Kind             string    `json:"kind"`
	Name             string    `json:"name"`
	DisplayName      string    `json:"display_name,omitempty"`
	Description      string    `json:"description,omitempty"`
	IndexDocument    string    `json:"index_document,omitempty"`
	NotFoundDocument string    `json:"not_found_document,omitempty"`
	CreatedAt        time.Time `json:"created_at"`
	UpdatedAt        time.Time `json:"updated_at"`
}

type SubscriptionShare added in v0.2.19

type SubscriptionShare struct {
	ID            string      `json:"id"`
	SchemaVersion int         `json:"schema_version"`
	Slug          string      `json:"slug"`
	Token         string      `json:"token"`
	Source        ShareSource `json:"source"`
	DefaultFormat string      `json:"default_format,omitempty"`
	Enabled       bool        `json:"enabled"`
	CreatedAt     time.Time   `json:"created_at"`
	UpdatedAt     time.Time   `json:"updated_at"`
	RotatedAt     *time.Time  `json:"rotated_at,omitempty"`
	ExpiresAt     *time.Time  `json:"expires_at,omitempty"`

	// Extra holds fields written by a newer schema version. It exists so a
	// rollback cannot silently delete data this version cannot interpret.
	Extra map[string]json.RawMessage `json:"-"`
}

SubscriptionShare is one publicly reachable subscription URL. Token is the only secret; Slug is a label that reaches reverse-proxy logs and client screenshots and is never relied on for authorization.

func (SubscriptionShare) MarshalJSON added in v0.2.19

func (s SubscriptionShare) MarshalJSON() ([]byte, error)

MarshalJSON re-emits the unknown fields alongside the named ones. A known field always wins over a same-named Extra entry: the caller's edit must not be shadowed by whatever an older decode happened to stash.

func (*SubscriptionShare) UnmarshalJSON added in v0.2.19

func (s *SubscriptionShare) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes the named fields and keeps everything else in Extra.

type SubscriptionSnapshot added in v0.2.19

type SubscriptionSnapshot struct {
	SchemaVersion int `json:"schema_version"`
	// PluginID and SubscriptionID together identify what this is a snapshot of.
	PluginID       string `json:"plugin_id"`
	SubscriptionID string `json:"subscription_id"`
	// Raw is the provider's response body exactly as the plugin received it.
	Raw string `json:"raw"`
	// SourceVersion identifies the exact secret-free source state used to render
	// Raw. SourceManifest is its canonical, map-free provenance document. Neither
	// field may contain credentials or rendered subscription URIs.
	SourceVersion  string          `json:"source_version,omitempty"`
	SourceManifest json.RawMessage `json:"source_manifest,omitempty"`
	// Userinfo is the provider's Subscription-Userinfo header, passed through to
	// the client so its traffic display stays truthful. Empty when the provider
	// sent none.
	Userinfo  string    `json:"userinfo,omitempty"`
	FetchedAt time.Time `json:"fetched_at"`
	// FetchError records why the most recent refresh failed while this snapshot
	// stayed in service. A snapshot with a non-empty FetchError is still served -
	// that is the point - but the operator needs to see that it is stale.
	FetchError    string    `json:"fetch_error,omitempty"`
	LastAttemptAt time.Time `json:"last_attempt_at,omitempty"`
	// Stale is explicit in v2. It is independent of FetchError so public serving
	// code never has to infer whether preserved last-good content is current.
	Stale bool `json:"stale"`
	// contains filtered or unexported fields
}

SubscriptionSnapshot is the last content a plugin successfully fetched for one subscription, held by the CORE.

The core owns it because a plugin has nowhere durable to keep it: it cannot reach bolt, and its runtime working directory is deleted when the runner stops it. Raw is therefore an opaque blob from the core's point of view - it is stored, handed back on the next render, and never interpreted.

This record is what keeps clients served when a provider goes down or rotates its URL, so losing it means subscriptions go dark. It is durable, not a cache.

func (SubscriptionSnapshot) Clone added in v0.2.19

func (SubscriptionSnapshot) NeedsRewrite added in v0.2.19

func (s SubscriptionSnapshot) NeedsRewrite() bool

func (SubscriptionSnapshot) PersistedSchemaVersion added in v0.2.19

func (s SubscriptionSnapshot) PersistedSchemaVersion() int

func (*SubscriptionSnapshot) UnmarshalJSON added in v0.2.19

func (s *SubscriptionSnapshot) UnmarshalJSON(raw []byte) error

UnmarshalJSON performs the only supported in-memory schema migration. The durable store remains responsible for staging and atomically rewriting v1 records after every record has decrypted and validated.

type SubscriptionSourceManifestEdge added in v0.2.19

type SubscriptionSourceManifestEdge struct {
	Source              string `json:"source"`
	Target              string `json:"target"`
	Generation          uint64 `json:"generation"`
	ObservationRevision uint64 `json:"observation_revision"`
	Status              string `json:"status"`
}

type SubscriptionSourceManifestEndpoint added in v0.2.19

type SubscriptionSourceManifestEndpoint struct {
	LineUUID    string   `json:"line_uuid"`
	NodeID      string   `json:"node_id"`
	Label       string   `json:"label"`
	Host        string   `json:"host"`
	Port        int      `json:"port"`
	SNI         string   `json:"sni"`
	Fingerprint string   `json:"fingerprint"`
	ALPN        []string `json:"alpn"`
	PublicKey   string   `json:"public_key"`
	ShortID     string   `json:"short_id"`
	Flow        string   `json:"flow"`
}

type SubscriptionSourceManifestEntry added in v0.2.19

type SubscriptionSourceManifestEntry struct {
	Root     string                             `json:"root"`
	Endpoint SubscriptionSourceManifestEndpoint `json:"endpoint"`
	Path     []SubscriptionSourceManifestEdge   `json:"path"`
	Terminal SubscriptionSourceManifestTerminal `json:"terminal"`
}

type SubscriptionSourceManifestIdentity added in v0.2.19

type SubscriptionSourceManifestIdentity struct {
	ID         string `json:"id"`
	Generation uint64 `json:"generation"`
}

type SubscriptionSourceManifestTerminal added in v0.2.19

type SubscriptionSourceManifestTerminal struct {
	LineUUID            string `json:"line_uuid"`
	Generation          uint64 `json:"generation"`
	ObservationRevision uint64 `json:"observation_revision"`
	Status              string `json:"status"`
}

type SubscriptionSourceManifestV1 added in v0.2.19

type SubscriptionSourceManifestV1 struct {
	Schema     string                             `json:"schema"`
	Renderer   string                             `json:"renderer"`
	Identity   SubscriptionSourceManifestIdentity `json:"identity"`
	EntryRoots []string                           `json:"entry_roots"`
	Entries    []SubscriptionSourceManifestEntry  `json:"entries"`
}

func DecodeSubscriptionSourceManifest added in v0.2.19

func DecodeSubscriptionSourceManifest(raw []byte) (SubscriptionSourceManifestV1, error)

func (SubscriptionSourceManifestV1) Clone added in v0.2.19

func (SubscriptionSourceManifestV1) Validate added in v0.2.19

func (m SubscriptionSourceManifestV1) Validate() error

type Task

type Task struct {
	ID            string               `json:"id"`
	ApprovalID    string               `json:"approval_id,omitempty"`
	ActorID       string               `json:"actor_id"`
	TokenID       string               `json:"token_id"`
	Targets       []string             `json:"targets"`
	Interpreter   string               `json:"interpreter"`
	Script        string               `json:"script"`
	TimeoutSec    int                  `json:"timeout_sec"`
	OutputLimit   int                  `json:"output_limit"`
	Status        string               `json:"status"`
	LeaseID       string               `json:"lease_id,omitempty"`
	LeasedBy      string               `json:"leased_by,omitempty"`
	TargetLeases  map[string]TaskLease `json:"target_leases,omitempty"`
	RerunOfTaskID string               `json:"rerun_of_task_id,omitempty"`
	RerunOfNodeID string               `json:"rerun_of_node_id,omitempty"`
	CreatedAt     time.Time            `json:"created_at"`
	StartedAt     time.Time            `json:"started_at,omitempty"`
	FinishedAt    time.Time            `json:"finished_at,omitempty"`
}

type TaskLease added in v0.2.10

type TaskLease struct {
	LeaseID   string    `json:"lease_id"`
	StartedAt time.Time `json:"started_at,omitempty"`
}

type TaskResult

type TaskResult struct {
	TaskID     string    `json:"task_id"`
	LeaseID    string    `json:"lease_id,omitempty"`
	NodeID     string    `json:"node_id"`
	ExitCode   int       `json:"exit_code"`
	Stdout     string    `json:"stdout"`
	Stderr     string    `json:"stderr"`
	Error      string    `json:"error"`
	StartedAt  time.Time `json:"started_at"`
	FinishedAt time.Time `json:"finished_at"`
}

type TerminalEvent added in v0.2.3

type TerminalEvent struct {
	Seq       int64     `json:"seq"`
	Kind      string    `json:"kind"`
	Data      string    `json:"data,omitempty"`
	CreatedAt time.Time `json:"created_at"`
}

type TerminalInput added in v0.2.3

type TerminalInput struct {
	Seq       int64     `json:"seq"`
	Kind      string    `json:"kind"`
	Data      string    `json:"data,omitempty"`
	Cols      int       `json:"cols,omitempty"`
	Rows      int       `json:"rows,omitempty"`
	CreatedAt time.Time `json:"created_at"`
}

type TerminalSession added in v0.2.3

type TerminalSession struct {
	ID        string    `json:"id"`
	NodeID    string    `json:"node_id"`
	ActorID   string    `json:"actor_id,omitempty"`
	TokenID   string    `json:"token_id,omitempty"`
	Shell     string    `json:"shell,omitempty"`
	Cols      int       `json:"cols,omitempty"`
	Rows      int       `json:"rows,omitempty"`
	Status    string    `json:"status"`
	Error     string    `json:"error,omitempty"`
	BytesIn   int64     `json:"bytes_in,omitempty"`
	BytesOut  int64     `json:"bytes_out,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	OpenedAt  time.Time `json:"opened_at,omitempty"`
	ClosedAt  time.Time `json:"closed_at,omitempty"`
	LastSeen  time.Time `json:"last_seen,omitempty"`
}

type Token

type Token struct {
	ID              string    `json:"id"`
	Name            string    `json:"name"`
	TokenHash       string    `json:"token_hash"`
	ActorID         string    `json:"actor_id"`
	Scopes          []string  `json:"scopes"`
	ServerAllowlist []string  `json:"server_allowlist"`
	RevokedAt       time.Time `json:"revoked_at,omitempty"`
	CreatedAt       time.Time `json:"created_at"`
}

type TraceAgentConfig added in v0.2.19

type TraceAgentConfig struct {
	Policy   TracePolicy         `json:"policy"`
	Sessions []TraceAgentSession `json:"sessions,omitempty"`
	// ServerTime lets the agent bound clock skew when enforcing TTLs.
	ServerTime time.Time `json:"server_time,omitzero"`
	// RawSourceID is the virtual log source that receives the node's ordinary
	// sing-box lines, the ones no capture session asked for. They go to the
	// existing bounded log store rather than the trace store, so the Logs view
	// keeps working and there is parser evidence to look at after the fact
	// instead of only assembled records. Empty means the server does not want
	// raw lines from this node.
	RawSourceID string `json:"raw_source_id,omitempty"`
}

TraceAgentConfig is what an agent polls: its node policy plus the sessions whose filters touch this node, already expanded by the server into predicates this node can evaluate locally.

type TraceAgentSession added in v0.2.19

type TraceAgentSession struct {
	ID        string     `json:"id"`
	Level     TraceLevel `json:"level"`
	ExpiresAt time.Time  `json:"expires_at"`
	// UserNames are sing-box inbound user names (u_<16hex>, or a legacy label).
	UserNames []string `json:"user_names,omitempty"`
	// InboundTags are sing-box inbound tags on THIS node.
	InboundTags []string `json:"inbound_tags,omitempty"`
	DstPatterns []string `json:"dst_patterns,omitempty"`
}

TraceAgentSession is one session as the agent sees it: the server has already turned user ids into the u_<hex> names this node actually renders, and line uuids into inbound tags.

func (TraceAgentSession) MatchesAll added in v0.2.19

func (s TraceAgentSession) MatchesAll() bool

MatchesAll reports whether this session captures everything on the node.

type TraceBatch added in v0.2.19

type TraceBatch struct {
	NodeID  string       `json:"node_id"`
	Records []ConnRecord `json:"records,omitempty"`
	Lines   []TraceLine  `json:"lines,omitempty"`
	// CoreGeneration and CoreStartedAt let the server detect a restart even if
	// it missed the records that the restart swept.
	CoreGeneration uint64    `json:"core_generation,omitempty"`
	CoreStartedAt  time.Time `json:"core_started_at,omitzero"`
	// Dropped counts what the agent discarded under budget since the last batch.
	Dropped uint64 `json:"dropped,omitempty"`
	// Unparsed counts lines the parser could not read. A rising number means
	// sing-box changed its format, which is the failure mode most likely to be
	// mistaken for "nothing is happening".
	Unparsed   uint64    `json:"unparsed,omitempty"`
	CapturedAt time.Time `json:"captured_at"`
}

TraceBatch is the agent to server envelope.

func (TraceBatch) SourceLooksBare added in v0.2.19

func (b TraceBatch) SourceLooksBare() bool

SourceLooksBare reports whether this value is the zero batch. The agent sends {node_id, batch}; a client that posts a bare TraceBatch instead still authenticates, because TraceBatch has its own NodeID, and then decodes into an empty Batch that the server would accept with 200 OK and zero records. Nothing would ever surface that. A batch that legitimately carries only counters still sets CapturedAt, so this stays false for it.

type TraceFilter added in v0.2.19

type TraceFilter struct {
	UserIDs   []string `json:"user_ids,omitempty"`
	LineUUIDs []string `json:"line_uuids,omitempty"`
	NodeIDs   []string `json:"node_ids,omitempty"`
	// DstPatterns are case-insensitive substrings matched against the
	// destination host. Not globs: a substring is what an operator types.
	DstPatterns []string `json:"dst_patterns,omitempty"`
}

TraceFilter selects what a session captures. Empty fields mean "no constraint on this dimension"; a filter with every field empty matches the whole node, which is legal but is what the budget exists to survive.

func (TraceFilter) IsEmpty added in v0.2.19

func (f TraceFilter) IsEmpty() bool

IsEmpty reports whether the filter constrains nothing.

type TraceLevel added in v0.2.19

type TraceLevel string

TraceLevel is the verbosity of a sing-box log subscription. It is NOT the node's log.level (which stays at whatever the config says and governs only what lands on disk).

const (
	// TraceLevelInfo carries connection accept, authenticated user, outbound,
	// and close. It is enough to assemble a ConnRecord and is the always-on
	// default.
	TraceLevelInfo TraceLevel = "info"
	// TraceLevelDebug adds rule matches, sniffing results, and DNS.
	TraceLevelDebug TraceLevel = "debug"
	// TraceLevelTrace adds the cancel-path close lines.
	TraceLevelTrace TraceLevel = "trace"
)

type TraceLine added in v0.2.19

type TraceLine struct {
	SessionID string    `json:"session_id"`
	NodeID    string    `json:"node_id"`
	Seq       uint64    `json:"seq"`
	At        time.Time `json:"at"`
	Level     string    `json:"level"`
	LogID     uint32    `json:"log_id,omitempty"`
	Tag       string    `json:"tag,omitempty"`
	Message   string    `json:"message"`
	// Raw is the payload exactly as sing-box sent it, kept so a parser bug can
	// never destroy the evidence it failed to read.
	Raw string `json:"raw,omitempty"`
}

TraceLine is one raw sing-box log line kept because a session asked for it. Unlabelled lines do not come here; they stay in the existing logstore.

type TraceMarker added in v0.2.19

type TraceMarker struct {
	Kind   string    `json:"kind"`
	At     time.Time `json:"at"`
	NodeID string    `json:"node_id,omitempty"`
	UserID string    `json:"user_id,omitempty"`
	Title  string    `json:"title"`
	Detail string    `json:"detail,omitempty"`
	// Count is the blast radius where one applies: connections closed by a
	// restart, nodes touched by an apply.
	Count         int    `json:"count,omitempty"`
	CorrelationID string `json:"correlation_id,omitempty"`
}

TraceMarker is one event on the timeline.

type TracePolicy added in v0.2.19

type TracePolicy struct {
	NodeID  string `json:"node_id"`
	Enabled bool   `json:"enabled"`
	// Level is the floor for this node. The agent subscribes at the max of this
	// and every running session's level.
	Level TraceLevel `json:"level"`
	// BudgetLinesPerSec caps ingest for this node. Over budget the agent drops
	// oldest and counts; the count is reported and displayed, never hidden.
	BudgetLinesPerSec int `json:"budget_lines_per_sec"`
	// ClashAPIAddr is the loopback address of the node's sing-box Clash API,
	// e.g. "127.0.0.1:9090". Never a routable address.
	ClashAPIAddr string `json:"clash_api_addr,omitempty"`
	// SecretPath is where the agent reads the Clash API secret on the node. The
	// secret itself never travels to the server and is never stored here.
	SecretPath string    `json:"secret_path,omitempty"`
	UpdatedAt  time.Time `json:"updated_at,omitzero"`

	// LastCoreGeneration and LastCoreStartedAt are the newest sing-box process
	// instance this node reported. A change is a restart, and recording it here
	// means a restart on an idle node is still visible even though it swept no
	// connections.
	LastCoreGeneration uint64    `json:"last_core_generation,omitempty"`
	LastCoreStartedAt  time.Time `json:"last_core_started_at,omitzero"`
}

TracePolicy is the per-node collection policy: the always-on floor. It is an agent behaviour setting, not a node config change, so it is audited but does not go through the approval chain.

type TraceSession added in v0.2.19

type TraceSession struct {
	ID     string      `json:"id"`
	Name   string      `json:"name"`
	Filter TraceFilter `json:"filter"`
	Level  TraceLevel  `json:"level"`

	StartedAt time.Time `json:"started_at"`
	ExpiresAt time.Time `json:"expires_at"`
	EndedAt   time.Time `json:"ended_at,omitzero"`
	State     string    `json:"state"`

	StartedBy     string `json:"started_by"`
	CorrelationID string `json:"correlation_id,omitempty"`

	// Counters, reported back for display. Dropped is as load-bearing as Lines:
	// a capture that silently lost lines reads as a quiet network.
	Lines   uint64 `json:"lines"`
	Records uint64 `json:"records"`
	Dropped uint64 `json:"dropped"`
}

TraceSession is a time-boxed capture. The TTL is enforced on the agent as well as the server, so a session still ends if the control plane goes away mid-capture. That is deliberate: an unbounded trace left running on a node is a privacy and disk problem nobody would notice.

func (TraceSession) Active added in v0.2.19

func (s TraceSession) Active(t time.Time) bool

Active reports whether the session should still be capturing at t.

type TunnelIngress added in v0.2.0

type TunnelIngress struct {
	Hostname string `json:"hostname"`
	Service  string `json:"service"`
	Path     string `json:"path,omitempty"`
}

TunnelIngress maps a public hostname to a node-local service for a Cloudflare Tunnel. Service is a cloudflared service URL, e.g. http://localhost:8088, ssh://localhost:22, or the literal http_status:404.

type TunnelProfile added in v0.2.0

type TunnelProfile struct {
	ID              string          `json:"id"`
	Name            string          `json:"name"`
	NodeID          string          `json:"node_id"`
	TunnelID        string          `json:"tunnel_id"`
	CredentialsFile string          `json:"credentials_file,omitempty"`
	Ingress         []TunnelIngress `json:"ingress"`
	CreatedAt       time.Time       `json:"created_at"`
	UpdatedAt       time.Time       `json:"updated_at"`
}

TunnelProfile describes a Cloudflare Tunnel deployed on a node. Credentials are node-local (CredentialsFile path); the server only stores the topology.

type User

type User struct {
	ID           string   `json:"id"`
	Username     string   `json:"username"`
	PasswordHash string   `json:"password_hash"`
	Scopes       []string `json:"scopes"`
	// ServerAllowlist confines this operator to a set of nodes, exactly as the
	// same field on Token confines an API caller. Empty means every node, which
	// is what every existing account has and so preserves their access across
	// the upgrade. "*" is accepted as an explicit spelling of the same thing.
	//
	// Without this a human account could only be narrowed by scope, never by
	// node: the confinement machinery existed and was enforced everywhere, but
	// only an API token could express it.
	ServerAllowlist    []string `json:"server_allowlist,omitempty"`
	TOTPEnabled        bool     `json:"totp_enabled"`
	TOTPSecret         string   `json:"totp_secret,omitempty"`
	RecoveryCodeHashes []string `json:"recovery_code_hashes,omitempty"`
	// LastTOTPStep is the highest RFC-6238 counter accepted for this user. A
	// successful verification must present a strictly greater step, which makes
	// each code single-use and prevents replay within the validity window.
	LastTOTPStep uint64 `json:"last_totp_step,omitempty"`
	// SecurityEpoch is bumped on password change, 2FA disable, or admin revoke.
	// Sessions carry the epoch at which they were minted; a session whose epoch
	// is older than the user's current epoch is rejected, so privilege-reducing
	// events invalidate all existing sessions.
	SecurityEpoch uint64    `json:"security_epoch,omitempty"`
	CreatedAt     time.Time `json:"created_at"`
}

type WGExternalPeer added in v0.2.17

type WGExternalPeer struct {
	ID           string    `json:"id"`
	NetworkID    string    `json:"network_id"`
	Name         string    `json:"name"`
	Address      string    `json:"address"`
	PublicKey    string    `json:"public_key"`
	AllowedIPs   []string  `json:"allowed_ips,omitempty"`
	LastIssuedAt time.Time `json:"last_issued_at,omitempty"`
	CreatedAt    time.Time `json:"created_at"`
	UpdatedAt    time.Time `json:"updated_at"`
}

WGExternalPeer is a non-node device (laptop, phone). Its private key is generated server-side, rendered once for the operator, and never persisted: only the public key and metadata survive. Regeneration mints a new key and invalidates the old config.

type WGMembership added in v0.2.17

type WGMembership struct {
	NetworkID     string `json:"network_id"`
	NodeID        string `json:"node_id"`
	Address       string `json:"address"`
	Role          string `json:"role"` // hub | spoke | peer
	InterfaceName string `json:"interface_name,omitempty"`
	ListenPort    int    `json:"listen_port,omitempty"`
	// Endpoint is the peer dial address rendered into WireGuard configs. It must
	// be host:port, with IPv6 hosts bracketed as in net.JoinHostPort.
	Endpoint        string   `json:"endpoint,omitempty"`
	Keepalive       int      `json:"keepalive,omitempty"`
	MTU             int      `json:"mtu,omitempty"`
	ExtraAllowedIPs []string `json:"extra_allowed_ips,omitempty"`
	Version         int64    `json:"version"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

WGMembership binds a node into a network. Address is server-allocated from the network CIDR and always pinned to a host route when it becomes a peer's AllowedIPs, so a member can never claim another member's address. ExtraAllowedIPs are additive routes a hub advertises (a LAN CIDR, or 0.0.0.0/0 for an exit node); they are reviewed in the plan like any other mutation and may never widen another member's pinned address.

type WGNetwork added in v0.2.17

type WGNetwork struct {
	ID          string   `json:"id"`
	Name        string   `json:"name"`
	CIDR        string   `json:"cidr"`
	Topology    string   `json:"topology"` // mesh | hub-and-spoke | custom
	ListenPort  int      `json:"listen_port,omitempty"`
	Keepalive   int      `json:"keepalive,omitempty"`
	MTU         int      `json:"mtu,omitempty"`
	DNS         []string `json:"dns,omitempty"`
	Description string   `json:"description,omitempty"`
	Version     int64    `json:"version"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

WGNetwork is a named WireGuard network. A fleet may run several; a node may belong to more than one (one interface per membership).

type WorkerScript

type WorkerScript struct {
	ID           string    `json:"id"`
	Name         string    `json:"name"`
	Source       string    `json:"source"`
	Capabilities []string  `json:"capabilities"`
	Public       bool      `json:"public"`
	UpdatedAt    time.Time `json:"updated_at"`
}

Jump to

Keyboard shortcuts

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