Documentation
¶
Overview ¶
Package server is the Halos console backend: fleet ingest, policy and experiment read APIs, and the embedded web console.
Index ¶
- Constants
- Variables
- func AppendAuditOffline(dir, actor, action, target string, details map[string]string, now time.Time) error
- func CheckSeparateClone(repoDir, servedDir string) error
- func LoadVerdicts(path string) (map[string]Verdict, error)
- func VerifyAuditChain(entries []AuditEntry) error
- type AssignmentRow
- type AuditEntry
- type AuditResponse
- type Catalog
- type CatalogItem
- type ChannelRelease
- type Config
- type ConvergenceView
- type Device
- type DeviceDetail
- type ExperimentDetail
- type ExperimentView
- type FleetResponse
- type GitPolicyWriter
- func (w *GitPolicyWriter) Propose(ctx context.Context, org *policy.Org, req Request, approver string) (string, error)
- func (w *GitPolicyWriter) ProposeRollout(ctx context.Context, plan func(string) (*promote.Change, error), ...) (string, error)
- func (w *GitPolicyWriter) ProposeStatus(ctx context.Context, experiment, status, title, body string) (string, error)
- type GuardrailVerdict
- type HarnessPin
- type HarnessStatus
- type Host
- type IssueView
- type Launch
- type LauncherSetup
- type Me
- type MyDevice
- type MyDevices
- type PatchSummary
- type PolicyView
- type PolicyWriter
- type Portal
- type Principal
- type ReleasesResponse
- type Report
- type Request
- type RingRelease
- type RingStats
- type RingView
- type RolloutView
- type Server
- func (s *Server) Audit(ctx context.Context, actor, action, target string, details map[string]string)
- func (s *Server) AuditStrict(ctx context.Context, actor, action, target string, details map[string]string) error
- func (s *Server) Close() error
- func (s *Server) Handler() http.Handler
- func (s *Server) KillExperiment(ctx context.Context, name, actor, reason string) (bool, error)
- func (s *Server) ReloadPolicy()
- type Store
- type ToggleKill
- type TogglePayload
- type TogglePreview
- type ToggleView
- type Verdict
- type WhoAmI
Constants ¶
const ( StatusPending = "pending" StatusApproving = "approving" // transient, in-memory only: a PR is being opened StatusApproved = "approved" StatusDenied = "denied" )
Request statuses.
const DefaultDeviceTTL = 90 * 24 * time.Hour
DefaultDeviceTTL is how long a device token works after enrollment.
const DefaultPostureMaxAge = 45 * time.Minute
DefaultPostureMaxAge is how old a device's last report may be before its posture is stale: three default halod intervals (15m).
const ( // ExpiryWarning is how close to expiry a ring pointer is flagged. ExpiryWarning = 48 * time.Hour )
const MaxHistory = 50
MaxHistory bounds the per-reporter report history.
const MaxReportBytes = 64 << 10
MaxReportBytes caps a POST /fleet/report body.
Variables ¶
var ErrManual = errors.New("no automatic policy change for this request kind")
ErrManual: approval is recorded but a human must make the change.
Functions ¶
func AppendAuditOffline ¶
func AppendAuditOffline(dir, actor, action, target string, details map[string]string, now time.Time) error
AppendAuditOffline appends one entry to <dir>/audit.jsonl using the server's hash chain, for tools that act on the data dir without a running server (halo controller run). Single-writer: the chain head is read from disk on open, so it must not run while halo-server is appending to the same dir (the server caches seq/head in memory and the chain would fork). Run the standalone controller only against a stopped server, or a separate data dir.
func CheckSeparateClone ¶
CheckSeparateClone refuses a writer clone that is, contains, or sits inside the served policy directory (symlinks resolved): proposals reset and branch the clone, and the served policy must never show unmerged changes.
func LoadVerdicts ¶
LoadVerdicts reads a JSON array of Verdict, keyed by experiment name. A missing path or file is not an error (no analysis yet).
func VerifyAuditChain ¶
func VerifyAuditChain(entries []AuditEntry) error
VerifyAuditChain checks seq continuity, prev links and every entry hash. ponytail: truncating the newest entries is undetectable from the log alone; pin AuditResponse.Head externally if that matters.
Types ¶
type AssignmentRow ¶
type AssignmentRow struct {
Experiment string `json:"experiment"`
Status string `json:"status"`
Variant string `json:"variant,omitempty"`
Control bool `json:"control,omitempty"`
}
AssignmentRow is one experiment's outcome for the subject; Variant is empty when not enrolled.
type AuditEntry ¶
type AuditEntry struct {
Seq uint64 `json:"seq"`
Time time.Time `json:"time"`
Actor string `json:"actor"`
Action string `json:"action"`
Target string `json:"target,omitempty"`
IP string `json:"ip,omitempty"`
RequestID string `json:"requestId,omitempty"`
Details map[string]string `json:"details,omitempty"`
Prev string `json:"prev"`
Hash string `json:"hash"`
}
AuditEntry is one privileged action. Hash covers every other field, Prev is the previous entry's Hash (empty for the first), so editing, reordering or removing any entry but the newest breaks the chain from that point on.
type AuditResponse ¶
type AuditResponse struct {
Entries []AuditEntry `json:"entries"`
Next uint64 `json:"next"` // pass as ?since= to get entries after these
Total int `json:"total"`
Verified bool `json:"verified"` // whole chain, not just this page
VerifyError string `json:"verifyError,omitempty"`
Head string `json:"head"`
}
AuditResponse is GET /api/v1/audit.
type Catalog ¶
type Catalog struct {
Harnesses []HarnessPin `json:"harnesses"`
Launchers []string `json:"launchers"`
Items []CatalogItem `json:"items"`
}
Catalog is GET /api/v1/catalog.
type CatalogItem ¶
type CatalogItem struct {
Kind string `json:"kind"`
Item string `json:"item"`
Desc string `json:"desc,omitempty"`
}
CatalogItem is something the user can request.
type ChannelRelease ¶
type ChannelRelease struct {
Channel string `json:"channel"`
Published bool `json:"published"`
Digest string `json:"digest,omitempty"`
Seq uint64 `json:"seq,omitempty"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
Expired bool `json:"expired"`
Stale bool `json:"stale"`
Suspicious bool `json:"suspicious"`
Error string `json:"error,omitempty"`
Devices int `json:"devices"` // ring hosts reporting this channel's digest
}
ChannelRelease is one experiment channel of a ring's current release.
type Config ¶
type Config struct {
PolicyDir string
Token string // bearer token required by POST /api/v1/fleet/report; must be non-empty
VerdictsPath string // optional JSON file written by `halo exp analyze`
Store Store
Log *slog.Logger
Now func() time.Time
Web fs.FS // nil = placeholder page (defaults to the embedded console)
// TrustedProxies are the reverse proxies whose X-Forwarded-For is believed
// for per-client rate limiting. Empty = use the TCP peer address only.
TrustedProxies []netip.Prefix
// Portal auth/self-service. SessionKey signs cookies (>=32 bytes); required unless DevUser is set.
SessionKey []byte
OIDCClientSecret string
Portal Portal
DataDir string // request/device/session-revocation logs ("" = in-memory)
DeviceTTL time.Duration // device token lifetime after enrollment (0 = DefaultDeviceTTL)
Writer PolicyWriter // nil: approvals are recorded but need manual policy changes
// Kill switch. KillStore nil = opened from DataDir. GET
// /api/v1/gateway/killswitch is served only when both KillKey (signs the
// list) and GatewayToken (gateways' bearer token) are set; GET
// /api/v1/fleet/killswitch (device token) whenever KillKey is set.
KillStore *controller.KillStore
KillKey ed25519.PrivateKey
GatewayToken string
// PostureMaxAge: a device whose last report is older is stale (GET
// /api/v1/gateway/posture, served whenever GatewayToken is set). 0 = DefaultPostureMaxAge.
PostureMaxAge time.Duration
// DevUser disables authentication and acts as this user. INSECURE: local demos only.
DevUser string
DevGroups []string
DevAdmin bool
}
Config wires a Server.
type ConvergenceView ¶
type ConvergenceView struct {
Total int `json:"total"`
OnDigest int `json:"onDigest"`
Other int `json:"other"`
Digests map[string]int `json:"digests"`
}
ConvergenceView counts a ring's devices on the ring's current digest vs others.
type Device ¶
type Device struct {
ID string `json:"id"` // uuid
Hash string `json:"hash,omitempty"`
UserID string `json:"userID"`
Groups []string `json:"groups,omitempty"` // IdP groups at enrollment; ring is otherwise resolved live
CreatedAt time.Time `json:"createdAt"`
LastSeen time.Time `json:"lastSeen"`
// LastAuth is when the user last authenticated for this device (enrollment);
// Groups date from then. The token stops working at ExpiresAt: re-enroll.
LastAuth time.Time `json:"lastAuth,omitzero"`
ExpiresAt time.Time `json:"expiresAt,omitzero"`
Revoked bool `json:"revoked"`
}
Device is the server-side binding of a device token. Only the SHA-256 of the token is ever stored; the token itself is shown once, in the enrollment response.
type DeviceDetail ¶
type DeviceDetail struct {
Device Device `json:"device"`
Last *Host `json:"last,omitempty"`
History []Host `json:"history"` // newest first, at most MaxHistory
}
DeviceDetail is GET /api/v1/devices/{id}: the binding, its latest report and recent history.
type ExperimentDetail ¶
type ExperimentDetail struct {
ExperimentView
Verdict *Verdict `json:"verdict,omitempty"`
}
ExperimentDetail is an experiment plus its latest verdict, if any.
type ExperimentView ¶
type ExperimentView struct {
Name string `json:"name"`
Type policy.ExperimentType `json:"type"`
Axis policy.Axis `json:"axis"`
Status string `json:"status"`
Rings []string `json:"rings"`
Variants []policy.Variant `json:"variants"`
Sample float64 `json:"sampleRate,omitempty"`
Metrics policy.Metrics `json:"metrics"`
Stopping policy.Stopping `json:"stopping"`
}
type FleetResponse ¶
type FleetResponse struct {
Hosts []Host `json:"hosts"`
Rings []RingStats `json:"rings"`
Total int `json:"total"`
Drift int `json:"driftHosts"`
}
FleetResponse is GET /api/v1/fleet.
type GitPolicyWriter ¶
type GitPolicyWriter struct {
RepoDir string
ServedDir string // the served policy dir; Propose refuses if it overlaps RepoDir
Subdir string // policy root inside the repo ("" = repo root)
Base string // base branch, default "main"
Opener promote.PROpener
// Git runs git in RepoDir and returns trimmed stdout (default: os/exec); injectable for tests.
Git func(ctx context.Context, args ...string) (string, error)
// contains filtered or unexported fields
}
GitPolicyWriter implements PolicyWriter by editing YAML in a dedicated clone of the policy repo and handing the files to a promote.PROpener (GHOpener shells `git` + `gh`). It only opens PRs; a human merges.
RepoDir MUST be a separate clone from the served policy directory (ServedDir): each proposal hard-resets RepoDir to origin/<Base> and PROpener checks out a branch there, and the served policy must never show unmerged changes.
func (*GitPolicyWriter) ProposeRollout ¶
func (w *GitPolicyWriter) ProposeRollout(ctx context.Context, plan func(string) (*promote.Change, error), branch, title string, body func(string) string) (string, error)
ProposeRollout implements controller.RolloutWriter on this clone (same lock and sync as ProposeStatus).
func (*GitPolicyWriter) ProposeStatus ¶
func (w *GitPolicyWriter) ProposeStatus(ctx context.Context, experiment, status, title, body string) (string, error)
ProposeStatus implements controller.Writer on this clone, so the console and halo-server's experiment controller share one clone, one lock and one status-PR implementation (controller.GitWriter / promote.PlanStatus).
type GuardrailVerdict ¶
type HarnessPin ¶
HarnessPin is one harness at the version pinned by the user's ring profile.
type HarnessStatus ¶
HarnessStatus mirrors cmd/halod's per-harness report.
type Launch ¶
type Launch struct {
Launcher string `json:"launcher"`
Filename string `json:"filename,omitempty"` // devcontainer
Snippet string `json:"snippet,omitempty"` // devcontainer
URL string `json:"url,omitempty"` // codespaces, coder
Token string `json:"token,omitempty"` // laptop
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
TTLSeconds int `json:"ttlSeconds,omitempty"`
Bash string `json:"bash,omitempty"`
PowerShell string `json:"powershell,omitempty"`
}
Launch is the POST /api/v1/launch/{launcher} response; fields depend on the launcher.
type LauncherSetup ¶
type LauncherSetup struct {
Launcher string `json:"launcher"`
Missing string `json:"missing"` // what the operator must change in the portal config
}
LauncherSetup is a policy-enabled launcher this server is not configured to run.
type Me ¶
type Me struct {
Principal
Ring string `json:"ring"`
Release string `json:"release,omitempty"`
Variants []AssignmentRow `json:"variants"`
Profile *profileSummary `json:"profile,omitempty"`
SelfService bool `json:"selfService"`
Launchers []string `json:"launchers"` // enabled in policy AND configured on this server
// LauncherSetup lists enabled launchers this server cannot run yet (admins only).
LauncherSetup []LauncherSetup `json:"launcherSetup,omitempty"`
Requestable []string `json:"requestable"`
OptInRings []string `json:"optInRings"` // rings the user may still ask to join
SupportURL string `json:"supportURL,omitempty"`
DevInsecure bool `json:"devInsecure,omitempty"`
}
Me is GET /api/v1/me.
type MyDevice ¶ added in v0.2.0
type MyDevice struct {
Device Device `json:"device"`
Last *Host `json:"last,omitempty"`
Expired bool `json:"expired"` // the token no longer works: re-enroll
}
MyDevice is one of the caller's own enrolled devices with its latest report.
type MyDevices ¶ added in v0.2.0
type MyDevices struct {
Devices []MyDevice `json:"devices"`
Posture gateway.PostureVerdict `json:"posture"`
}
MyDevices is GET /api/v1/me/devices: the caller's devices and the posture verdict the gateway would give them right now (the same check that answers a 403 "device posture check failed").
type PatchSummary ¶
type PatchSummary struct {
MCPServers []string `json:"mcpServers,omitempty"` // "name url"
Hooks []string `json:"hooks,omitempty"` // "event matcher"
Env []string `json:"env,omitempty"` // variable names
Overrides []string `json:"overrides,omitempty"` // top-level keys
}
PatchSummary is one harness's fragment, as names.
type PolicyView ¶
type PolicyView struct {
Org string `json:"org"`
Gateway *policy.Gateway `json:"gateway,omitempty"`
Rings []RingView `json:"rings"`
Profiles []string `json:"profiles"`
Experiments []ExperimentView `json:"experiments"`
Issues []IssueView `json:"issues"`
}
PolicyView is the JSON shape of GET /api/v1/policy.
type PolicyWriter ¶
type PolicyWriter interface {
Propose(ctx context.Context, org *policy.Org, req Request, approver string) (prURL string, err error)
}
PolicyWriter turns an approved request into a reviewable change to the policy repo. Implementations must never merge. ErrManual means no automatic patch exists for the kind.
type Portal ¶
type Portal struct {
// BaseURL is the externally visible URL of this server; baked into launch artifacts and enroll scripts.
BaseURL string `json:"baseURL"`
// Registry is the OCI repository halod pulls releases from.
Registry string `json:"registry"`
// RegistryPlainHTTP reads the registry over http (local/dev registries only).
RegistryPlainHTTP bool `json:"registryPlainHTTP"`
// PubKeyFile is the release-verification public key (PEM); served at /enroll/release.pub.
PubKeyFile string `json:"pubKeyFile"`
// DevcontainerFeature is the OCI ref of the halos Dev Container Feature.
DevcontainerFeature string `json:"devcontainerFeature"`
DevcontainerImage string `json:"devcontainerImage"`
// CodespacesURL / CoderURL are URL templates; {ring} and {user} are query-escaped.
CodespacesURL string `json:"codespacesURL"`
CoderURL string `json:"coderURL"`
// HalodURL is the halod download template with {os} and {arch}; HalodSHA256 pins each "<os>-<arch>" build.
HalodURL string `json:"halodURL"`
HalodSHA256 map[string]string `json:"halodSHA256"`
// PolicyRepoDir is a dedicated git clone the PolicyWriter branches from (never the served policy dir).
PolicyRepoDir string `json:"policyRepoDir"`
PolicySubdir string `json:"policySubdir"`
PolicyBase string `json:"policyBase"`
// SupportURL is where developers go when something breaks (a Slack channel, a ticket queue); shown on the kiosk.
SupportURL string `json:"supportURL"`
// Secrets are referenced by file path so they never sit in the JSON itself.
SessionKeyFile string `json:"sessionKeyFile"`
OIDCClientSecretFile string `json:"oidcClientSecretFile"`
PubKeyPEM string `json:"-"` // loaded from PubKeyFile by the caller
}
Portal is the self-service section of the server config (JSON file, --portal-config).
type Principal ¶
type Principal struct {
ID string `json:"id"`
Groups []string `json:"groups"`
Admin bool `json:"admin"`
}
Principal is the authenticated portal user.
type ReleasesResponse ¶
type ReleasesResponse struct {
Rings []RingRelease `json:"rings"`
Stale bool `json:"stale"` // some ring shows cached state because the registry could not be read
Registry string `json:"registry"`
}
ReleasesResponse is GET /api/v1/releases.
type Report ¶
type Report struct {
Time time.Time `json:"time"`
Hostname string `json:"hostname"`
User string `json:"user"`
Device string `json:"device,omitempty"` // set by the server for device-token reports
Ring string `json:"ring"`
Digest string `json:"digest"`
Harnesses map[string]HarnessStatus `json:"harnesses"`
Drift []string `json:"drift"`
LastError string `json:"lastError,omitempty"`
ErrorCode string `json:"errorCode,omitempty"` // classifies LastError, e.g. untrusted_owner, sandbox_unavailable, no_subject_for_experiment
// Experiment/Variant: the client-axis experiment variant the host applied.
Experiment string `json:"experiment,omitempty"`
Variant string `json:"variant,omitempty"`
Killed bool `json:"killed,omitempty"` // Experiment is kill-switched; the host runs the ring release
}
Report is the status payload halod POSTs (cmd/halod.Status).
type Request ¶
type Request struct {
ID string `json:"id"`
User string `json:"user"`
Kind string `json:"kind"` // mcp-server | model | ring-opt-in | harness
Item string `json:"item"`
Justification string `json:"justification"`
Ring string `json:"ring,omitempty"` // requester's ring at request time
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
DecidedBy string `json:"decidedBy,omitempty"`
DecidedAt *time.Time `json:"decidedAt,omitempty"`
PRURL string `json:"prURL,omitempty"`
Note string `json:"note,omitempty"`
}
Request is a developer's ask for access; approval opens a policy-repo PR, never a merge.
type RingRelease ¶
type RingRelease struct {
Ring string `json:"ring"`
Published bool `json:"published"` // a verified pointer exists
Org string `json:"org,omitempty"`
Digest string `json:"digest,omitempty"`
Seq uint64 `json:"seq,omitempty"`
IssuedAt *time.Time `json:"issuedAt,omitempty"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
ExpiresInSec int64 `json:"expiresInSeconds,omitempty"`
Expired bool `json:"expired"`
ExpiringSoon bool `json:"expiringSoon"` // <48h left (and not yet expired)
Stale bool `json:"stale"`
Suspicious bool `json:"suspicious"` // registry served a foreign-org or regressed pointer; last good shown
Error string `json:"error,omitempty"`
Channels []ChannelRelease `json:"channels"` // client-axis experiment channels the ring's release routes to
Convergence ConvergenceView `json:"convergence"` // hosts on the ring digest or one of its channels' digests are converged
Drift int `json:"driftHosts"`
}
RingRelease is one ring's row in GET /api/v1/releases.
type RingStats ¶
type RingStats struct {
Ring string `json:"ring"`
Hosts int `json:"hosts"`
Percent float64 `json:"percent"` // of fleet
Drift int `json:"driftHosts"`
Digests map[string]int `json:"digests"`
Versions map[string]map[string]int `json:"versions"` // harness -> installed version -> hosts
}
RingStats aggregates hosts by ring x release digest x harness version.
type RolloutView ¶
type RolloutView struct {
Rollout *policy.Rollout `json:"rollout"`
Timeline rollout.Timeline `json:"timeline"`
State *rollout.State `json:"state,omitempty"`
StateError string `json:"stateError,omitempty"`
}
RolloutView is one entry of GET /api/v1/rollouts: the definition, its plan, and the controller's state (last decision, gate values, hash-chained history) when this server's data dir holds it (halo-server --controller).
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is the HTTP API + console.
func (*Server) Audit ¶
func (s *Server) Audit(ctx context.Context, actor, action, target string, details map[string]string)
Audit is the best-effort form (login/logout): failures are logged, not returned. ctx carries the remote IP and request id when it derives from an HTTP request.
func (*Server) AuditStrict ¶
func (s *Server) AuditStrict(ctx context.Context, actor, action, target string, details map[string]string) error
AuditStrict records a privileged action and returns the append error. Call it for actions that must not go unrecorded and fail the request on error.
func (*Server) Close ¶
Close releases the files New opened under Config.DataDir. A Store or KillStore passed in via Config stays the caller's to close.
func (*Server) KillExperiment ¶
KillExperiment trips the kill switch as actor (the in-process controller) and audit-logs it. changed is false when it was already killed. The argument order is controller.Killer's (experiment, then actor): main wires it as a controller.KillFunc, where swapped strings would still compile.
func (*Server) ReloadPolicy ¶
func (s *Server) ReloadPolicy()
ReloadPolicy forces a policy reload on next read (wire to SIGHUP).
type Store ¶
type Store interface {
Put(Host) error
All() []Host // sorted by hostname
// History returns up to MaxHistory recent reports for key (see Host.key), newest first.
History(key string) []Host
}
Store keeps the latest report per reporter: per device ID for device-token reports, per hostname for fleet-token reports (see Host.key).
func OpenLogStore ¶
OpenLogStore replays dir/reports.jsonl (last line per host wins), rewrites it compacted, and appends from then on.
type ToggleKill ¶
type ToggleKill struct {
By string `json:"by"`
Reason string `json:"reason,omitempty"`
At time.Time `json:"at"`
}
ToggleKill is the live kill state of a toggle.
type TogglePayload ¶
type TogglePayload struct {
Harnesses map[string]PatchSummary `json:"harnesses,omitempty"`
Routes map[string]string `json:"routes,omitempty"` // alias -> "upstream/model" (or its targets)
}
TogglePayload summarises what a toggle delivers without its secrets: header and env values are never shown, override values only as key names.
type TogglePreview ¶
type TogglePreview struct {
Subject toggle.Subject `json:"subject"`
Decision toggle.Decision `json:"decision"`
}
TogglePreview is the "who gets it?" answer of GET /api/v1/toggles/{name}?user=.
type ToggleView ¶
type ToggleView struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Owner string `json:"owner"`
Expires string `json:"expires,omitempty"`
Stale bool `json:"stale"`
Axis policy.Axis `json:"axis"`
Default bool `json:"default"`
Rules []policy.ToggleRule `json:"rules"`
Payload TogglePayload `json:"payload"`
// Kill is set while the toggle is on the signed kill list.
Kill *ToggleKill `json:"kill,omitempty"`
}
ToggleView is the JSON shape of a toggle in GET /api/v1/toggles.
type Verdict ¶
type Verdict struct {
Experiment string `json:"experiment"`
Verdict string `json:"verdict"` // promote | rollback | continue | expired
Reason string `json:"reason"`
Control string `json:"control"`
Treatment string `json:"treatment"`
NControl int `json:"nControl"`
NTreatment int `json:"nTreatment"`
PValue float64 `json:"pValue"`
Effect float64 `json:"effect"`
EffectLower *float64 `json:"effectLower,omitempty"`
EffectUpper *float64 `json:"effectUpper,omitempty"`
Guardrails []GuardrailVerdict `json:"guardrails"`
EvaluatedAt *time.Time `json:"evaluatedAt,omitempty"`
}
Verdict is the console's view of an experiment analysis (a local view model mirroring promote.Report, plus an optional primary-effect CI for the forest plot). Keys are camelCase; encoding/json also matches promote.Report's untagged field names.