server

package
v0.6.1 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: Apache-2.0 Imports: 72 Imported by: 0

Documentation

Overview

Package server is the Halos console backend: fleet ingest, policy and experiment read APIs, and the embedded web console.

Index

Constants

View Source
const (
	// BreakGlassSessionTTL is the fixed lifetime of a break-glass session.
	BreakGlassSessionTTL = 30 * time.Minute
	// BreakGlassMinPassword is the shortest password HashBreakGlassPassword accepts.
	BreakGlassMinPassword = 16
	// BreakGlassIterations is the PBKDF2-HMAC-SHA256 work factor of new hashes
	// (OWASP 2023); a configured hash below breakGlassMinIterations is refused.
	BreakGlassIterations = 600_000
)
View Source
const (
	DefaultAPIRate  = 20
	DefaultAPIBurst = 100
)

DefaultAPIRate and DefaultAPIBurst are halo-server's per-principal budget on console/API routes: generous for the console (a page load fans out to a few dozen calls) and for automation, tight enough that one leaked token or runaway script cannot monopolise a replica.

View Source
const (
	RoleViewer   = "viewer"
	RoleOperator = "operator"
	RoleApprover = "approver"
	RoleAuditor  = "auditor"
	RoleAdmin    = "admin"
)

Roles (ADR-0015). Policy identity.roles maps IdP groups to them; identity.adminGroups also grants RoleAdmin.

View Source
const (
	StatusPending   = "pending"
	StatusApproving = "approving" // transient, in-memory only: a PR is being opened
	StatusApproved  = "approved"
	StatusDenied    = "denied"
)

Request statuses.

View Source
const (
	// ServiceTokenPrefix starts every token, so secret scanners can match it:
	// halo_sat_[0-9a-f]{80}.
	ServiceTokenPrefix = "halo_sat_"
	// DefaultServiceTokenMaxTTL is the longest lifetime, and the default, when
	// Config.ServiceTokenMaxTTL is 0.
	DefaultServiceTokenMaxTTL = 90 * 24 * time.Hour
	// MaxServiceTokenMaxTTL bounds Config.ServiceTokenMaxTTL.
	MaxServiceTokenMaxTTL = 365 * 24 * time.Hour

	// ServiceAccountPrefix starts a service account's principal id (and audit actor).
	ServiceAccountPrefix = "service-account:"
)
View Source
const (
	KindServiceAccount = "service-account"
	KindBreakGlass     = "break-glass"
)

Principal kinds; empty is a human console session.

View Source
const (
	DefaultSessionTTL  = 12 * time.Hour
	DefaultSessionIdle = time.Hour
	MinSessionTTL      = 5 * time.Minute
	MaxSessionTTL      = 7 * 24 * time.Hour
	MinSessionIdle     = time.Minute
)

Session policy (ADR-0036): an absolute lifetime fixed at login and an idle timeout that slides with use. Both are enforced from the signed cookie, so every replica agrees without shared state.

View Source
const DefaultCheckpointInterval = 5 * time.Minute

DefaultCheckpointInterval is how often a signed audit checkpoint is appended when new entries arrived (ADR-0041).

View Source
const DefaultDeviceTTL = 90 * 24 * time.Hour

DefaultDeviceTTL is how long a device token works after enrollment.

View Source
const DefaultGraphURL = "https://graph.microsoft.com/v1.0"

DefaultGraphURL is Microsoft Graph's v1.0 endpoint (Config.EntraGraphURL).

View Source
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).

View Source
const DefaultRetentionInterval = time.Hour

DefaultRetentionInterval is how often the retention sweep runs.

View Source
const (

	// ExpiryWarning is how close to expiry a ring pointer is flagged.
	ExpiryWarning = 48 * time.Hour
)
View Source
const MaxHistory = 50

MaxHistory bounds the per-reporter report history.

View Source
const MaxReportBytes = 64 << 10

MaxReportBytes caps a POST /fleet/report body.

View Source
const MinAuditRetention = 30 * 24 * time.Hour

MinAuditRetention is the shortest audit retention accepted: entries must outlive their export to the SIEM by a wide margin.

Variables

View Source
var DefaultSAMLGroupsAttributes = []string{
	"groups", "Group", "memberOf",
	"http://schemas.microsoft.com/ws/2008/06/identity/claims/groups",
	"http://schemas.xmlsoap.org/claims/Group",
}

DefaultSAMLGroupsAttributes are the attribute Names (never FriendlyName, which is informational and not unique) read as groups when SAMLConfig.GroupsAttributes is empty: Okta and generic (groups, Group), Ping and LDAP-backed IdPs (memberOf), Entra ID and ADFS.

View Source
var ErrManual = errors.New("no automatic policy change for this request kind")

ErrManual: approval is recorded but a human must make the change.

Roles lists every role in display order (policy.ConsoleRoles, which validates identity.roleBindings; TestRolesMatchPolicy keeps them equal).

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. It never rotates; the server rotates on its next append.

func CheckBreakGlassCIDRs added in v0.5.0

func CheckBreakGlassCIDRs(cidrs []netip.Prefix) error

CheckBreakGlassCIDRs refuses allowed ranges too broad to protect the lockout (the halo-server flag and New both apply it).

func CheckSeparateClone

func CheckSeparateClone(repoDir, servedDir string) error

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 HashBreakGlassPassword added in v0.5.0

func HashBreakGlassPassword(password string) (string, error)

HashBreakGlassPassword returns the configuration string for password.

func LoadExport added in v0.6.0

func LoadExport(path string) (map[uint64]ExportedEntry, error)

LoadExport reads a SIEM export for AuditVerifyOptions.Export. ponytail: holds two hex digests per exported entry in memory (about 150 bytes each); split the export by seq range if it is too large for that.

func LoadVerdicts

func LoadVerdicts(path string) (map[string]Verdict, error)

LoadVerdicts reads a JSON array of Verdict, keyed by experiment name. A missing path or file is not an error (no analysis yet).

func OpenAPI added in v0.5.0

func OpenAPI() ([]byte, error)

OpenAPI builds the OpenAPI 3.1 description of this server's HTTP API.

What is generated and what is written down:

  • the permission each cookie route needs comes from routePerms and the roles that hold it from rolePerms, so they cannot be mistyped here;
  • request and response schemas come from the Go types the handlers use (openapi_schema.go), so a new field appears on the next generation;
  • paths, summaries, query parameters, status codes and examples live in the apiOps table (openapi_ops.go). TestOpenAPI* pin that table to the mux (every registered route is documented, every documented route exists), to routePerms, to the Go types (examples decode strictly) and to the live handlers (every operation is called and must answer a documented status with a body matching its schema).

api/openapi.yaml is this function's output; `make openapi` rewrites it and TestOpenAPIFileCurrent fails while it is stale.

func VerifyAuditChain

func VerifyAuditChain(entries []AuditEntry) error

VerifyAuditChain checks seq continuity, prev links, every entry hash, redactions and retention cuts. ponytail: truncating the newest entries is undetectable from the log alone; anchor on a signed checkpoint your SIEM holds (ADR-0041, `halo audit verify --anchor`) 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 AuditConfig added in v0.3.0

type AuditConfig struct {
	Rotation AuditRotation `json:"rotation"`
	Export   AuditExport   `json:"export"`
}

AuditConfig is halo-server's --audit-config file (ADR-0016): rotation of the hash-chained audit log and its export to SIEM sinks.

func ParseAuditConfig added in v0.3.0

func ParseAuditConfig(r io.Reader) (AuditConfig, error)

ParseAuditConfig decodes and validates an --audit-config file. Unknown fields are errors, so a typo cannot silently disable a sink.

func (AuditConfig) Validate added in v0.3.0

func (c AuditConfig) Validate() error

Validate checks the config without reading any secret.

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"`
	Redacted  bool              `json:"redacted,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.

Redacted marks an entry whose personal data an erasure pseudonymised (ADR-0040). Its Hash is the original one, which still links the chain; its content is vouched for instead by a later subject.erase entry listing its seq and redaction digest. omitempty keeps every other entry's hash as it was.

func ChainAuditEntry added in v0.3.0

func ChainAuditEntry(e AuditEntry, seq uint64, head string) AuditEntry

ChainAuditEntry returns e as the entry after (seq, head): UTF-8 repaired, Seq, Prev and Hash set. Every backend chains through it.

func LastCheckpoint added in v0.6.0

func LastCheckpoint(path string, v bundle.Verifier) (e AuditEntry, ok bool, err error)

LastCheckpoint returns the newest checkpoint entry in an export file, to pass as AuditVerifyOptions.Anchor. Its own hash must match its content and, with v, its signature must verify; ok is false when the file holds none.

type AuditExport added in v0.3.0

type AuditExport struct {
	Sinks               []AuditSink `json:"sinks,omitempty"`
	BatchSize           int         `json:"batchSize,omitempty"`           // entries per delivery; default 100, max 1000
	PollIntervalSeconds int         `json:"pollIntervalSeconds,omitempty"` // idle re-check; appends wake sinks at once; default 5
	MaxBackoffSeconds   int         `json:"maxBackoffSeconds,omitempty"`   // retry backoff cap; default 300
}

AuditExport forwards every audit entry, at least once and in order, to each sink. Each sink has its own cursor, so one sink being down holds back only itself.

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, every segment, not just this page
	VerifyError string       `json:"verifyError,omitempty"`
	Head        string       `json:"head"`
	// ADR-0041: Signed is true when checkpoint signing is on; Checkpoints
	// counts the checkpoints that verified (their signatures too when Signed),
	// LastCheckpoint is the seq of the newest one whose signature verified.
	Signed         bool   `json:"signed,omitempty"`
	Checkpoints    int    `json:"checkpoints,omitempty"`
	LastCheckpoint uint64 `json:"lastCheckpoint,omitempty"`
}

AuditResponse is GET /api/v1/audit.

type AuditRotation added in v0.3.0

type AuditRotation struct {
	MaxBytes      int64 `json:"maxBytes,omitempty"`
	MaxAgeSeconds int64 `json:"maxAgeSeconds,omitempty"`
}

AuditRotation bounds the active segment (<data-dir>/audit.jsonl). When the next append would take it past MaxBytes, or its first entry is MaxAgeSeconds old, it is renamed to audit-<first seq, 20 digits>.jsonl and a new one is started. The chain does not restart: the first entry of the new segment links to the last of the previous one. Zero disables either bound.

type AuditSink added in v0.3.0

type AuditSink struct {
	Name   string `json:"name"`             // [a-z0-9-], unique: names the cursor file and the metrics label
	Type   string `json:"type"`             // webhook | splunk-hec | syslog
	Format string `json:"format,omitempty"` // native (default: the stored entry, byte for byte) | ocsf

	URL        string               `json:"url,omitempty"`        // webhook, splunk-hec: https only
	Headers    map[string]SecretRef `json:"headers,omitempty"`    // webhook: e.g. DD-API-KEY
	HMACSecret *SecretRef           `json:"hmacSecret,omitempty"` // webhook: X-Halo-Signature over timestamp.body
	Token      *SecretRef           `json:"token,omitempty"`      // splunk-hec (required)
	Index      string               `json:"index,omitempty"`      // splunk-hec
	SourceType string               `json:"sourceType,omitempty"` // splunk-hec; default halo:audit

	Address  string `json:"address,omitempty"`  // syslog: host:port, RFC 5425 (TLS, octet counting)
	Facility int    `json:"facility,omitempty"` // syslog: 0 = 13 (log audit)

	CAFile         string `json:"caFile,omitempty"`   // trust this CA bundle instead of the system roots
	CertFile       string `json:"certFile,omitempty"` // client certificate (mTLS), with KeyFile
	KeyFile        string `json:"keyFile,omitempty"`
	TimeoutSeconds int    `json:"timeoutSeconds,omitempty"` // per delivery; default 10
}

AuditSink is one export destination. Credentials are SecretRefs (an environment variable or a file), never literals in the config.

type AuditVerifyOptions added in v0.6.0

type AuditVerifyOptions struct {
	// Verifier checks checkpoint signatures (ADR-0041); nil checks links only.
	Verifier bundle.Verifier
	// Anchor is a checkpoint entry held outside the log (the newest one your
	// SIEM received): the log must still hold it unchanged, so cutting entries
	// from the end, or recomputing the chain, is caught.
	Anchor *AuditEntry
	// Export is the SIEM's copy (LoadExport): every seq both hold must have
	// the same hash and content, unless the log's entry is a vouched-for
	// redaction, and the log must reach the export's last seq. Unlike Anchor,
	// this compares content, not just the newest checkpoint.
	Export map[uint64]ExportedEntry
	// RequireCurrentKey: the newest verified checkpoint must be signed by the
	// Verifier's first key (the current one): a leaked previous key cannot
	// rewrite the tail after a rotation.
	RequireCurrentKey bool
}

AuditVerifyOptions tune the offline verifiers (`halo audit verify`).

type AuditVerifyResult added in v0.3.0

type AuditVerifyResult struct {
	Entries        uint64 `json:"entries"`
	Segments       int    `json:"segments"` // files read, the active one included
	Head           string `json:"head"`
	Error          string `json:"error,omitempty"`
	First          uint64 `json:"first,omitempty"` // seq of the oldest entry, when retention removed older ones
	Checkpoints    int    `json:"checkpoints,omitempty"`
	LastCheckpoint uint64 `json:"lastCheckpoint,omitempty"`
	UnsignedTail   uint64 `json:"unsignedTail,omitempty"`
	Anchored       bool   `json:"anchored,omitempty"` // the anchor was found unchanged
	// RedactedSinceExport counts entries that differ from the SIEM export only
	// because an erasure redacted them (each vouched for).
	RedactedSinceExport int `json:"redactedSinceExport,omitempty"`
}

AuditVerifyResult is what VerifyAuditDir reports. With a verifier, Checkpoints counts the checkpoints whose signature verified and UnsignedTail the entries after the last of them: what could have been cut or rewritten without breaking a signature, unless an anchor pins it.

func VerifyAuditBackend added in v0.3.0

func VerifyAuditBackend(ctx context.Context, b Backend, o AuditVerifyOptions) (AuditVerifyResult, error)

VerifyAuditBackend verifies the whole shared chain, streaming and uncached (`halo audit verify --database-url-file`). Segments is 0: there are none.

func VerifyAuditDir added in v0.3.0

func VerifyAuditDir(dir string, o AuditVerifyOptions) (AuditVerifyResult, error)

VerifyAuditDir verifies the chain across every segment in dir (rotated segments oldest first, then audit.jsonl), streaming and uncached. It only reads, so it is safe against a live server's data dir. err is an I/O error; a broken chain is AuditVerifyResult.Error.

func VerifyAuditExport added in v0.6.0

func VerifyAuditExport(path string, o AuditVerifyOptions) (AuditVerifyResult, error)

VerifyAuditExport verifies a file of exported entries (one native entry per line, or OCSF events whose raw_data is one) as a SIEM received them. An export may start after genesis: its first entry is taken as the start.

type AuthMethods added in v0.6.0

type AuthMethods struct {
	OIDC bool `json:"oidc"`
	SAML bool `json:"saml"`
}

AuthMethods is GET /auth/methods.

type Backend added in v0.3.0

type Backend interface {
	storage.Opener // device, request, session-revocation, SCIM logs
	storage.KV     // enrollment tokens, login states, server-side sessions, claims, workload jti
	// AppendAudit chains e after the current head under a lock held across
	// replicas, filling Seq, Prev and Hash.
	AppendAudit(ctx context.Context, e AuditEntry) error
	// EachAudit streams entries with seq >= from, oldest first, with their
	// stored bytes, until fn returns false. A row that does not decode is
	// passed zero-valued, so verification breaks there.
	EachAudit(ctx context.Context, from uint64, fn func(raw []byte, e AuditEntry) bool) error
	// EachAuditDesc streams entries newest first until fn returns false,
	// reading only as many rows as it is given (paged ORDER BY seq DESC).
	EachAuditDesc(ctx context.Context, fn func(raw []byte, e AuditEntry) bool) error
	// AuditHead is the newest entry's seq and hash.
	AuditHead(ctx context.Context) (uint64, string, error)
	// RolloutStates is where the controller's rollout state lives.
	RolloutStates() rollout.StateStore
	// Docs lists every document of kind (verdicts: one per experiment).
	Docs(ctx context.Context, kind string) ([][]byte, error)
	// Leased runs fn on exactly one replica at a time (lease name) until ctx
	// ends; docs writes through the view fn gets are fenced by that lease.
	Leased(ctx context.Context, name string, fn func(ctx context.Context, docs rollout.Docs))
	// ApplyBudget adds usage unless batch was applied before (atomic, any replica).
	ApplyBudget(ctx context.Context, batch string, incs []pgstore.BudgetCounter) (duplicate bool, err error)
	// PruneBudgets drops buckets older than retention and stale batch ids,
	// on the store's clock and independent of any replica's policy.
	PruneBudgets(ctx context.Context, retention time.Duration) (int64, error)
	// BudgetCounters reads the given (budget, key) counters; nil = all.
	BudgetCounters(ctx context.Context, keys [][2]string) ([]pgstore.BudgetCounter, error)
	// ClaimPending makes requestID the one pending access request for dedupe
	// (atomic across replicas; see pgstore.DB.ClaimPending). ReleasePending drops it.
	ClaimPending(ctx context.Context, dedupe, requestID string, holder func(id string) (known, pending bool)) (bool, error)
	ReleasePending(ctx context.Context, dedupe, requestID string) error

	// Ping fails when the backend is unreachable (/readyz).
	Ping(ctx context.Context) error
	// contains filtered or unexported methods
}

Backend is the shared state that lets several halo-server replicas serve one fleet (ADR-0021). Config.Backend nil means the single-replica default: JSONL files under DataDir and in-process ephemeral state.

type BreakGlass added in v0.5.0

type BreakGlass struct {
	Enabled      bool
	User         string
	Hash         string
	AllowedCIDRs []netip.Prefix
}

BreakGlass configures the break-glass account. Hash is a HashBreakGlassPassword string; it is sealed (logins answer 404) unless Enabled. User defaults to "breakglass". AllowedCIDRs, when set, are the only client addresses (after TrustedProxies) that may use it: login from elsewhere answers 404 as if sealed, and a session used from elsewhere is refused, so attackers outside them can neither guess nor exhaust it.

type BreakGlassLogin added in v0.5.0

type BreakGlassLogin struct {
	User     string `json:"user"`
	Password string `json:"password"`
}

BreakGlassLogin is POST /auth/breakglass.

type BudgetsResponse added in v0.3.0

type BudgetsResponse struct {
	Budgets []budget.Status `json:"budgets"`
	At      time.Time       `json:"at"`
}

BudgetsResponse is GET /api/v1/budgets.

type Capabilities added in v0.3.0

type Capabilities struct {
	KillSwitch bool `json:"killSwitch"`
	// Roles and Permissions are the caller's (ADR-0015), so the console hides
	// actions the server would refuse. The server enforces regardless.
	Roles       []string `json:"roles"`
	Permissions []string `json:"permissions"`
	// Bindings say over which rings and units each role holds (ADR-0037);
	// a permission may hold only for some resources.
	Bindings []policy.Grant `json:"bindings"`
}

Capabilities is GET /api/v1/capabilities.

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"`
	// Ticket lists the patterns (regular expressions, matched whole) a change
	// ticket must match when this caller requests this item: the approval
	// rules (ADR-0030) covering what approving it changes. Empty: none needed.
	Ticket []string `json:"ticket,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 CloudAgentsView added in v0.4.0

type CloudAgentsView struct {
	Tasks  []cloudagents.Task  `json:"tasks"`
	Alerts []cloudagents.Alert `json:"alerts"`
	// Configured reports what halo-server can see and do: the webhook
	// receiver and each vendor API client.
	Configured map[string]bool `json:"configured"`
}

CloudAgentsView is GET /api/v1/cloud-agents.

type CloudKillRequest added in v0.4.0

type CloudKillRequest struct {
	Scope  string `json:"scope"`
	Name   string `json:"name"`
	Reason string `json:"reason"`
	DryRun *bool  `json:"dryRun,omitempty"`
}

CloudKillRequest is POST /api/v1/cloud-agents/kill. DryRun defaults to true.

type CloudKillResponse added in v0.4.0

type CloudKillResponse struct {
	DryRun       bool                 `json:"dryRun"`
	Actions      []cloudagents.Action `json:"actions"`
	Unattributed int                  `json:"unattributed"`
	NextSteps    []string             `json:"next_steps"`
}

CloudKillResponse is the kill's plan (dry run) or outcome.

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
	// APIRate and APIBurst are each authenticated principal's request budget
	// on console/API routes (requests per second and burst); 0 = unlimited.
	APIRate  float64
	APIBurst int

	// 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)
	// Backend, when set, replaces DataDir and in-process state with shared
	// state so several replicas can serve (ADR-0021).
	Backend   Backend
	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 signs the kill list: a file key, or a KMS / external signer
	// (internal/signer, ADR-0038), whose signatures are cached briefly.
	KillKey      bundle.Signer
	GatewayToken string
	// Optional per-capability gateway tokens. When one is set it is the only
	// token that capability accepts (the shared GatewayToken no longer does);
	// empty falls back to GatewayToken. Each also enables its endpoint alone.
	GatewayKillswitchToken string
	GatewayPostureToken    string
	GatewayBudgetToken     string
	GatewayDirectoryToken  string
	GatewayDLPToken        string // POST /api/v1/gateway/dlp/events (ADR-0033)
	// GatewayDLPTokens maps a gateway name to its own DLP token. An event
	// batch sent with one is attributed to that gateway (a gateway name in the
	// body that differs is refused); one sent with GatewayDLPToken or the
	// shared token is recorded as gateway:unverified with the name it claims.
	// Setting any enables the endpoint.
	GatewayDLPTokens map[string]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

	// Audit: log rotation and SIEM export (ADR-0016). Both need DataDir.
	// Export runs only while RunAuditExport does.
	Audit AuditConfig
	// Retention bounds the unbounded stores (ADR-0040); the zero value keeps
	// everything. The sweep runs only while RunMaintenance does.
	Retention Retention
	// AuditSigner signs audit checkpoints (ADR-0041): a file key, or a KMS /
	// external signer (internal/signer). nil leaves the chain unsigned.
	// Checkpoints are appended every AuditCheckpointInterval (0 =
	// DefaultCheckpointInterval) while RunMaintenance runs, and at rotation.
	AuditSigner             bundle.KeySigner
	AuditCheckpointInterval time.Duration
	// AuditPreviousKeys are earlier audit keys' public halves: checkpoints and
	// vouches they signed keep verifying after a rotation (ADR-0041).
	AuditPreviousKeys []bundle.PublicKeyVerifier
	// AuditSigningEnableHead allows writing the audit.signing.enabled marker
	// over a log that holds entries but was never signed (first-ever enable),
	// or recreating a missing signing record over a signed log, but only when
	// it equals the log's head seq at that moment (a stale value is refused),
	// and only once per process (ADR-0041). 0 = off.
	AuditSigningEnableHead uint64

	// SCIM 2.0 provisioning at /scim/v2 (ADR-0022). SCIMTokenSHA256 holds the
	// hex SHA-256 of each accepted bearer token (two while rotating); empty =
	// SCIM off. SCIMAuthoritative: the directory, not the login's groups claim
	// or a device's enrollment groups, decides who is active and their groups.
	// SCIMStore nil = opened from DataDir.
	SCIMTokenSHA256   []string
	SCIMAuthoritative bool
	SCIMStore         SCIMStore
	// WorkloadKey signs workload credentials minted by POST
	// /api/v1/token/exchange (ADR-0023); nil disables the endpoint.
	WorkloadKey ed25519.PrivateKey

	// Cloud agents (ADR-0034). CloudWebhookSecrets verify GitHub deliveries
	// to POST /api/v1/cloud-agents/webhooks/github (two while rotating; none =
	// 404). CloudClients cancel and pause tasks; a nil client is not attempted.
	CloudWebhookSecrets []string
	CloudClients        cloudagents.Clients

	// Admin access (ADR-0036). Session bounds console sessions (absolute TTL
	// and sliding idle timeout). ServiceTokenMaxTTL caps service-account token
	// lifetimes (0 = DefaultServiceTokenMaxTTL). BreakGlass is the sealed local
	// admin account. EntraGraphURL is where identity.groupsOverage: graph
	// resolves groups ("" = DefaultGraphURL).
	Session            SessionPolicy
	ServiceTokenMaxTTL time.Duration
	BreakGlass         BreakGlass
	EntraGraphURL      string
	// SAML turns on SAML 2.0 login next to OIDC (ADR-0039); nil = off.
	SAML *SAMLConfig

	// DevUser disables authentication and acts as this user. INSECURE: local demos only.
	// DevRoles are its roles (ADR-0015); DevAdmin adds RoleAdmin.
	DevUser   string
	DevGroups []string
	DevRoles  []string
	DevAdmin  bool
	// contains filtered or unexported fields
}

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 CreateServiceTokenRequest added in v0.5.0

type CreateServiceTokenRequest struct {
	Name        string          `json:"name"`
	Role        string          `json:"role"`
	Scope       json.RawMessage `json:"scope,omitempty"`
	Description string          `json:"description,omitempty"`
	Owner       string          `json:"owner,omitempty"`
	TTL         string          `json:"ttl,omitempty"`
}

CreateServiceTokenRequest is POST /api/v1/service-tokens. TTL is a Go duration (e.g. 720h); empty = the server's maximum. Scope (ADR-0037), {"rings": [...], "units": [...]}, bounds the role; absent is global. It is decoded strictly: a misspelt key is a 400, never a silently global token.

type CreateServiceTokenResponse added in v0.5.0

type CreateServiceTokenResponse struct {
	Token        string           `json:"token"`
	ServiceToken ServiceTokenInfo `json:"serviceToken"`
}

CreateServiceTokenResponse carries the token: shown once, never again.

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 EgressRequest added in v0.3.0

type EgressRequest struct {
	Agent string `json:"agent,omitempty"`
	URL   string `json:"url,omitempty"`
	Skill string `json:"skill,omitempty"`
}

EgressRequest is POST /api/v1/agents/egress: the remote agent by agents.remote name, or by a URL on its AgentCard's origin, and optionally the skill about to be invoked.

type EraseReport added in v0.6.0

type EraseReport struct {
	// Pseudonym replaced the subject in the records kept (audit entries,
	// requests the subject decided); it is random, so it cannot be linked back.
	Pseudonym string            `json:"pseudonym,omitempty"`
	DryRun    bool              `json:"dryRun,omitempty"`
	Erased    map[string]int    `json:"erased"`
	Retained  map[string]string `json:"retained"`
	Errors    map[string]string `json:"errors,omitempty"`
}

EraseReport is what an erasure removed or pseudonymised, per store.

type EraseRequest added in v0.6.0

type EraseRequest struct {
	User string `json:"user"`
	// Aliases are other identifiers of the same person to erase with User: an
	// OS user name or hostname fleet-token reports carry.
	Aliases []string `json:"aliases,omitempty"`
	Reason  string   `json:"reason"`
	DryRun  bool     `json:"dryRun,omitempty"`
}

EraseRequest is POST /api/v1/users/erase. The subject is in the body, not the path, so it never reaches request logs.

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 ExportedEntry added in v0.6.0

type ExportedEntry struct{ Hash, Digest string }

ExportedEntry is one entry of a SIEM export, as --compare-export loads it.

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 FreezeView added in v0.4.0

type FreezeView struct {
	Incident *policy.ActiveFreeze           `json:"incident"`
	Rings    map[string]policy.FreezeStatus `json:"rings"`
}

FreezeView is GET /api/v1/freeze.

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) FindPR added in v0.3.0

func (w *GitPolicyWriter) FindPR(ctx context.Context, marker string) (string, bool, error)

FindPR implements controller.PRFinder through the opener. It only queries the forge, so it does not take the clone lock.

func (*GitPolicyWriter) Propose

func (w *GitPolicyWriter) Propose(ctx context.Context, org *policy.Org, req Request, approver string) (string, error)

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 GuardrailVerdict struct {
	Metric string `json:"metric"`
	Result struct {
		Status        string  `json:"status"` // pass | fail | inconclusive
		Regression    float64 `json:"regression"`
		Lower         float64 `json:"lower"`
		Upper         float64 `json:"upper"`
		MaxRegression float64 `json:"maxRegression"`
	} `json:"result"`
}

type HarnessPin

type HarnessPin struct {
	Name    string `json:"name"`
	Version string `json:"version"`
}

HarnessPin is one harness at the version pinned by the user's ring profile.

type HarnessStatus

type HarnessStatus struct {
	Want      string `json:"want"`
	Installed string `json:"installed"`
}

HarnessStatus mirrors cmd/halod's per-harness report.

type Host

type Host struct {
	Report
	LastSeen time.Time `json:"lastSeen"`
}

Host is the latest report for one machine plus when the server saw it.

type ImportReport added in v0.3.0

type ImportReport struct {
	Copied  map[string]int64 // e.g. "journal/devices", "audit", "reports", "docs/rollout-state"
	Skipped map[string]int   // torn or undecodable source lines, per file
	Head    string           // audit chain head hash after the copy
}

ImportReport is what ImportDataDir copied, by the names pgstore.DB.Counts uses.

func ImportDataDir added in v0.3.0

func ImportDataDir(ctx context.Context, dir string, b *PGBackend) (ImportReport, error)

ImportDataDir copies a stopped single-replica server's data dir into an empty Postgres database, then verifies it: per-log record counts, report and rollout counts, and the whole audit chain (which must verify in the source too, and end at the same head). The source is only read. The copy and the verification run in one transaction, so an import that fails at any step leaves the database empty and can simply be run again.

type IssueView

type IssueView struct {
	Severity string `json:"severity"`
	Path     string `json:"path"`
	Message  string `json:"message"`
}

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
	Permissions []string        `json:"permissions"` // the console hides what these do not allow
	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 PGBackend added in v0.3.0

type PGBackend struct {
	*pgstore.DB
	LeaseTTL time.Duration // Leased lease length; 0 = 15s
}

PGBackend is the Postgres Backend.

func NewPGBackend added in v0.3.0

func NewPGBackend(db *pgstore.DB) *PGBackend

NewPGBackend wraps an open, migrated database.

func (*PGBackend) AppendAudit added in v0.3.0

func (b *PGBackend) AppendAudit(ctx context.Context, e AuditEntry) error

AppendAudit implements Backend.

func (*PGBackend) EachAudit added in v0.3.0

func (b *PGBackend) EachAudit(ctx context.Context, from uint64, fn func(raw []byte, e AuditEntry) bool) error

EachAudit implements Backend.

func (*PGBackend) EachAuditDesc added in v0.3.0

func (b *PGBackend) EachAuditDesc(ctx context.Context, fn func(raw []byte, e AuditEntry) bool) error

EachAuditDesc implements Backend.

func (*PGBackend) ImportAuditEntry added in v0.3.0

func (b *PGBackend) ImportAuditEntry(ctx context.Context, e AuditEntry) error

ImportAuditEntry appends an entry that is already chained (Seq, Prev and Hash set), as `halo-server import` copies a file chain; it must extend the head.

func (*PGBackend) Leased added in v0.3.0

func (b *PGBackend) Leased(ctx context.Context, name string, fn func(ctx context.Context, docs rollout.Docs))

Leased implements Backend with a leader lease per name.

func (*PGBackend) RolloutStates added in v0.3.0

func (b *PGBackend) RolloutStates() rollout.StateStore

RolloutStates implements Backend.

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 PendingErasure added in v0.6.0

type PendingErasure struct {
	ID          string    `json:"id"`
	RequestedBy string    `json:"requestedBy"`
	ExpiresAt   time.Time `json:"expiresAt"`
	Confirm     string    `json:"confirm"` // the route a different admin calls
	User        string    `json:"user,omitempty"`
	Aliases     []string  `json:"aliases,omitempty"`
	Reason      string    `json:"reason,omitempty"`
}

PendingErasure is the 202 answer, and GET /api/v1/users/erase/{id}: an erasure waiting for a second admin. User, Aliases and Reason are what the confirming admin must resubmit, unchanged.

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"`
	// PolicyForge (gh|github|gitlab; empty = autodetect from the clone's origin) and PolicyForgeBaseURL
	// (GitHub Enterprise Server / self-managed GitLab API root) choose where the writer opens its PRs/MRs.
	PolicyForge        string `json:"policyForge"`
	PolicyForgeBaseURL string `json:"policyForgeBaseURL"`
	// 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).

func (Portal) Validate

func (p Portal) Validate(devInsecure bool) error

Validate rejects anything that could inject into the generated shell/PowerShell scripts, and a non-https baseURL (session cookies and device tokens would travel in clear) unless it is loopback or devInsecure.

type Principal

type Principal struct {
	ID     string   `json:"id"`
	Groups []string `json:"groups"`
	Roles  []string `json:"roles"`
	Admin  bool     `json:"admin"` // holds RoleAdmin globally (a scoped admin shows in Bindings only, ADR-0037); kept for API clients that predate roles
	// Kind is empty for a human console session, else KindServiceAccount or
	// KindBreakGlass (ADR-0036).
	Kind string `json:"kind,omitempty"`
	// OnBehalfOf is the human who created a service token: separation-of-
	// duties checks treat the token as that person (ADR-0036).
	OnBehalfOf string `json:"onBehalfOf,omitempty"`
	// Bindings are the roles p holds and over which scope (ADR-0037); Roles
	// is their distinct role names. Authorization reads Bindings only.
	Bindings []policy.Grant `json:"bindings"`
}

Principal is the authenticated portal user. Roles are recomputed from current policy on every request, never trusted from the cookie.

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"`
	// Ticket is the change ticket reference (Jira, ServiceNow, ...), required
	// when an approval rule covering the requester asks for one (ADR-0030).
	Ticket string `json:"ticket,omitempty"`
	// Target is the requester's ring and unit chain when filed: what the
	// approval rules are matched against.
	Target *policy.ChangeTarget `json:"target,omitempty"`
	// Approvals recorded so far; the request is decided when they meet every
	// covering rule's quorum (one approval when no rule covers it).
	Approvals []policy.Approval `json:"approvals,omitempty"`
}

Request is a developer's ask for access; approval opens a policy-repo PR, never a merge.

type Retention added in v0.6.0

type Retention struct {
	FleetReports time.Duration // reporters not seen for this long, with their history
	Requests     time.Duration // decided access requests, from the decision
	Devices      time.Duration // revoked devices from their last use, expired ones from expiry
	Audit        time.Duration // audit entries; whole rotated segments on the file backend
	Interval     time.Duration // how often the sweep runs; 0 = DefaultRetentionInterval
}

Retention bounds how long halo-server keeps each unbounded store; a zero field keeps that store forever (the default, and the behaviour before ADR-0040). Ages are measured from the newest activity of a record.

type Revocation added in v0.4.0

type Revocation struct {
	JTI     string `json:"jti,omitempty"`
	Subject string `json:"subject,omitempty"`
	// RevokedBefore: a subject's credentials issued at or before it are dead.
	RevokedBefore *time.Time `json:"revokedBefore,omitempty"`
	Expires       time.Time  `json:"expires"` // the row is dropped then: nothing it matches can still be valid
	By            string     `json:"by"`
	Reason        string     `json:"reason"`
	At            time.Time  `json:"at"`
}

Revocation is one active revocation (GET /api/v1/revocations, and the answer to POST).

type RevocationsResponse added in v0.4.0

type RevocationsResponse struct {
	Revocations []Revocation `json:"revocations"`
}

RevocationsResponse is GET /api/v1/revocations.

type RevokeRequest added in v0.4.0

type RevokeRequest struct {
	JTI     string `json:"jti,omitempty"`
	Subject string `json:"subject,omitempty"`
	Reason  string `json:"reason"`
}

RevokeRequest is POST /api/v1/revocations: exactly one of JTI and Subject, and a Reason (recorded in the audit log).

type RevokeServiceTokenRequest added in v0.5.0

type RevokeServiceTokenRequest struct {
	Reason string `json:"reason"`
}

RevokeServiceTokenRequest is POST /api/v1/service-tokens/{id}/revoke.

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.

func Aggregate

func Aggregate(hosts []Host, order []string) []RingStats

Aggregate builds ring stats; rings named in order come first, others alphabetically.

type RingView

type RingView struct {
	Name       string            `json:"name"`
	Order      int               `json:"order"`
	Profile    string            `json:"profile"`
	Release    string            `json:"release,omitempty"`
	Membership policy.Membership `json:"membership"`
	Resolved   *policy.Profile   `json:"resolved,omitempty"`
	Error      string            `json:"error,omitempty"`
}

type RolloutApprovals added in v0.4.0

type RolloutApprovals struct {
	Rollout   string               `json:"rollout"`
	Step      string               `json:"step"`
	Target    policy.ChangeTarget  `json:"target"`
	Approvals []policy.Approval    `json:"approvals"`
	Quorum    *policy.QuorumResult `json:"quorum,omitempty"` // nil = no rule covers the next step
	Freeze    policy.FreezeStatus  `json:"freeze"`
}

RolloutApprovals is GET /api/v1/rollouts/{name}/approvals and the answer to an approval: the live step's approvals and what they add up to.

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 SAMLConfig added in v0.6.0

type SAMLConfig struct {
	// IdPMetadata is the IdP's SAML metadata XML (EntityDescriptor, or an
	// EntitiesDescriptor holding exactly one IdP).
	IdPMetadata []byte
	// Key and Cert are the SP key pair (RSA or ECDSA): AuthnRequests are
	// signed with it and EncryptedAssertions decrypted with it.
	Key  crypto.Signer
	Cert *x509.Certificate
	// EntityID is the SP entity id and the audience assertions must name;
	// "" = <baseURL>/auth/saml/metadata.
	EntityID string
	// UserAttribute is the Name of the attribute holding the user id; "" = the
	// NameID. It must be an identifier the user cannot edit at the IdP.
	UserAttribute string
	// GroupsAttributes are the Names of the attributes holding groups; empty =
	// DefaultSAMLGroupsAttributes.
	GroupsAttributes []string
	// AllowIdPInitiated accepts unsolicited responses (no AuthnRequest).
	// Off by default: they cannot be bound to the browser that logs in.
	AllowIdPInitiated bool
}

SAMLConfig turns on SAML login (ADR-0039); Config.SAML nil = off.

type SCIMEmail added in v0.3.0

type SCIMEmail struct {
	Value   string `json:"value"`
	Type    string `json:"type,omitempty"`
	Primary bool   `json:"primary,omitempty"`
}

SCIMEmail is one "emails" value.

type SCIMGroup added in v0.3.0

type SCIMGroup struct {
	ID          string    `json:"id"`
	ExternalID  string    `json:"externalId,omitempty"`
	DisplayName string    `json:"displayName"`
	Members     []string  `json:"members,omitempty"`
	Created     time.Time `json:"created"`
	Modified    time.Time `json:"lastModified"`
}

SCIMGroup is a provisioned group; Members are SCIMUser IDs. Policy refers to a group by DisplayName (ring membership, adminGroups, toggle rules).

type SCIMName added in v0.3.0

type SCIMName struct {
	Formatted  string `json:"formatted,omitempty"`
	GivenName  string `json:"givenName,omitempty"`
	FamilyName string `json:"familyName,omitempty"`
}

SCIMName is the RFC 7643 §4.1.1 "name" complex attribute (the parts Halos keeps).

type SCIMStore added in v0.3.0

type SCIMStore interface {
	Users() []SCIMUser   // sorted by userName
	Groups() []SCIMGroup // sorted by displayName
	User(id string) (SCIMUser, bool)
	Group(id string) (SCIMGroup, bool)
	PutUser(SCIMUser) error
	// DeleteUser also removes the user from every group.
	DeleteUser(id string) error
	PutGroup(SCIMGroup) error
	DeleteGroup(id string) error
	// Lookup resolves a login identity (userName, case-insensitive) to the
	// user and the displayNames of the groups they are a member of.
	Lookup(userName string) (u SCIMUser, groups []string, ok bool)
	Close() error
}

SCIMStore persists the SCIM directory. The SCIM handlers serialise every mutation, so an implementation only has to make each call atomic. An HA backend implements this same interface (ADR-0022).

func OpenSCIMStore added in v0.3.0

func OpenSCIMStore(dir string) (SCIMStore, error)

OpenSCIMStore opens dir/scim.jsonl, or a memory-only store when dir is "".

func OpenSCIMStoreOn added in v0.3.0

func OpenSCIMStoreOn(ctx context.Context, o storage.Opener, names storage.KV) (SCIMStore, error)

OpenSCIMStoreOn opens the directory on a shared backend's "scim" log (ADR-0021): every read first applies other replicas' writes, so a deprovisioning on one replica holds on all of them at once. The memberOf index is rebuilt by the same apply on replay and catch-up. names holds one claim per canonical userName, so two replicas can never provision the same name twice even if their uniqueness checks race.

type SCIMUser added in v0.3.0

type SCIMUser struct {
	ID          string      `json:"id"`
	ExternalID  string      `json:"externalId,omitempty"`
	UserName    string      `json:"userName"`
	DisplayName string      `json:"displayName,omitempty"`
	Name        *SCIMName   `json:"name,omitempty"`
	Emails      []SCIMEmail `json:"emails,omitempty"`
	Active      bool        `json:"active"`
	Created     time.Time   `json:"created"`
	Modified    time.Time   `json:"lastModified"`
}

SCIMUser is a provisioned user. UserName is the halo identity: it must equal the OIDC userClaim value (email by default) the console and gateway see.

type SecretRef added in v0.3.0

type SecretRef struct {
	Env  string `json:"env,omitempty"`
	File string `json:"file,omitempty"`
}

SecretRef names where a secret lives: exactly one of Env or File. The value is read once at startup and never logged or echoed in an error.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the HTTP API + console.

func New

func New(cfg Config) (*Server, error)

New validates cfg and returns a Server. It does not require the policy dir to load yet.

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) Checkpoint added in v0.6.0

func (s *Server) Checkpoint(ctx context.Context) (bool, error)

Checkpoint appends a signed checkpoint of the chain head, unless the head already is one (nothing happened since). It reports whether it appended.

func (*Server) Close

func (s *Server) Close() error

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) Handler

func (s *Server) Handler() http.Handler

Handler returns the full mux with security headers and request logging.

func (*Server) IncidentFreeze added in v0.4.0

func (s *Server) IncidentFreeze() (*policy.ActiveFreeze, error)

IncidentFreeze returns the incident freeze, or nil when none is set.

func (*Server) KillExperiment

func (s *Server) KillExperiment(ctx context.Context, name, actor, reason string) (bool, error)

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) Policy added in v0.3.0

func (s *Server) Policy() (*policy.Org, error)

Policy is the policy this server serves: the last load that passed policy.SecurityIssues (a refused or broken reload keeps the previous one), with the workload rule removals of a refused load applied (policy.TightenWorkloads). The in-process controller acts on it, so the leader never acts on a policy its own API, or any replica's, refuses.

func (*Server) ReloadPolicy

func (s *Server) ReloadPolicy()

ReloadPolicy forces a policy reload on next read (wire to SIGHUP).

func (*Server) RolloutGovernance added in v0.4.0

func (s *Server) RolloutGovernance(_ context.Context, org *policy.Org, r *policy.Rollout) (rollout.Governance, error)

RolloutGovernance is controller.Controller.Governance for this server: the incident freeze and the approvals recorded for r's live step.

func (*Server) RunAuditExport added in v0.3.0

func (s *Server) RunAuditExport(ctx context.Context)

RunAuditExport forwards the audit log to every configured sink until ctx ends. Call it once; it returns at once when no sink is configured.

With a shared Backend (ADR-0021) each sink forwards from exactly one replica at a time, under lease "audit-export-<sink>": its cursor is a document written through that lease's fence, so N replicas never deliver the same entries N times and a deposed forwarder cannot move the cursor back. Delivery stays at-least-once: a batch sent just before a failover is resent.

func (*Server) RunMaintenance added in v0.6.0

func (s *Server) RunMaintenance(ctx context.Context)

RunMaintenance runs the retention sweep and appends signed audit checkpoints until ctx ends; it returns at once when neither is configured. With a shared Backend it runs on one replica at a time (lease "maintenance"), as the controller and audit export do.

func (*Server) Sweep added in v0.6.0

func (s *Server) Sweep(ctx context.Context) (map[string]int, error)

Sweep applies the retention policy once and returns what it removed per store. Each removal is audited (actor halo-server). Errors of one store do not stop the others; they are joined.

func (*Server) WriteMetrics added in v0.3.0

func (s *Server) WriteMetrics(w io.Writer)

WriteMetrics writes halo-server's Prometheus text-format metrics.

type ServiceTokenInfo added in v0.5.0

type ServiceTokenInfo struct {
	ID           string           `json:"id"`
	Principal    string           `json:"principal"` // the audit actor its requests carry
	Name         string           `json:"name"`
	Role         string           `json:"role"`
	Scope        policy.RoleScope `json:"scope"` // {} = global (ADR-0037)
	Description  string           `json:"description,omitempty"`
	Owner        string           `json:"owner,omitempty"`
	CreatedBy    string           `json:"createdBy"`
	CreatedAt    time.Time        `json:"createdAt"`
	ExpiresAt    time.Time        `json:"expiresAt"`
	Expired      bool             `json:"expired"`
	Revoked      bool             `json:"revoked"`
	RevokedBy    string           `json:"revokedBy,omitempty"`
	RevokedAt    *time.Time       `json:"revokedAt,omitempty"`
	RevokeReason string           `json:"revokeReason,omitempty"`
}

ServiceTokenInfo is one token as the API shows it (never the secret or its hash).

type ServiceTokensResponse added in v0.5.0

type ServiceTokensResponse struct {
	Tokens []ServiceTokenInfo `json:"tokens"`
}

ServiceTokensResponse is GET /api/v1/service-tokens.

type SessionPolicy added in v0.5.0

type SessionPolicy struct {
	TTL  time.Duration
	Idle time.Duration
}

SessionPolicy bounds console sessions. TTL 0 = DefaultSessionTTL. Idle 0 = DefaultSessionIdle (capped at TTL); a negative Idle turns the idle timeout off.

type SessionsIngest added in v0.4.0

type SessionsIngest struct {
	Records []ledger.Record `json:"records"`
}

SessionsIngest is the body of POST /api/v1/sessions and its answer (the records as stored, Who.User set from the directory).

type SessionsResponse added in v0.4.0

type SessionsResponse struct {
	Sessions    []ledger.Stored `json:"sessions"`
	Total       int             `json:"total"` // matching sessions before limit
	Verified    bool            `json:"verified"`
	VerifyError string          `json:"verifyError,omitempty"`
	Head        string          `json:"head"`
}

SessionsResponse is GET /api/v1/sessions.

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 NewMemStore

func NewMemStore() Store

NewMemStore returns a non-persistent Store.

func NewPGStore added in v0.3.0

func NewPGStore(db *pgstore.DB) Store

NewPGStore returns the fleet report Store on db: the latest report per reporter, and a MaxHistory-bounded history that, unlike the file store's, survives a restart.

func OpenLogStore

func OpenLogStore(dir string) (Store, error)

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 TokenExchangeResponse added in v0.3.0

type TokenExchangeResponse struct {
	AccessToken     string   `json:"access_token"`
	IssuedTokenType string   `json:"issued_token_type"`
	TokenType       string   `json:"token_type"`
	ExpiresIn       int      `json:"expires_in"`
	Subject         string   `json:"subject"`
	Groups          []string `json:"groups,omitempty"`
	Rule            string   `json:"rule,omitempty"`
	Ring            string   `json:"ring,omitempty"`
	GatewayURL      string   `json:"gateway_url,omitempty"`
	// Actors (delegated credentials, ADR-0024) is the act chain, root-side
	// first; Models the credential's model scope (absent = not narrowed).
	Actors []string `json:"actors,omitempty"`
	Models []string `json:"models,omitempty"`
}

TokenExchangeResponse is the RFC 8693 response plus the routing facts the `halo ci token` client exports (subject, ring, gateway URL).

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.

type WhoAmI

type WhoAmI struct {
	User        string          `json:"user"`
	Groups      []string        `json:"groups"`
	Ring        string          `json:"ring"`
	Profile     string          `json:"profile,omitempty"`
	Release     string          `json:"release,omitempty"`
	Assignments []AssignmentRow `json:"assignments"`
}

WhoAmI is GET /api/v1/whoami.

Jump to

Keyboard shortcuts

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