onboard

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

Documentation

Overview

Package onboard is the deterministic core of `halo doctor` and `halo onboard`: detect the AI CLIs on this machine, check the environment and print exact fixes, plan and apply a local install of a ring's managed config, generate a local halo-proxy config, run a headless round trip, and generate a company rollout bundle. Agents (the halos-onboard skill, the MCP tools) call these; nothing here asks questions or decides policy.

Secrets: credential checks only report whether an environment variable is set, never its value, and verify output is redacted before it is returned.

Index

Constants

View Source
const (
	StatusOK   = "ok"
	StatusWarn = "warn"
	StatusFail = "fail"
)

Check statuses.

View Source
const (
	ActionCreate    = "create"
	ActionUpdate    = "update"
	ActionUnchanged = "unchanged"
	// ActionForeign is a differing file Halos did not write (e.g. one an MDM
	// pushed). Apply refuses to replace it unless asked to.
	ActionForeign = "foreign"
)

File actions in an install plan.

View Source
const (
	VerifyPass    = "pass"
	VerifyFail    = "fail"
	VerifySkipped = "skipped"
	VerifyDryRun  = "dry-run"
)

Verify results.

View Source
const (
	PolicyCreate    = "create"
	PolicyUnchanged = "unchanged"
	PolicyKept      = "kept" // exists and differs from the answers: never overwritten
)

Policy actions.

View Source
const DefaultLocalProxy = "127.0.0.1:8088"

DefaultLocalProxy is the loopback listener a local halo-proxy uses.

View Source
const LocalProxyDir = ".halos/local"

LocalProxyDir is where `halo onboard proxy` writes, inside the policy repo.

View Source
const (
	VerifyPrompt = "Reply with exactly the word HALOS_OK and nothing else."
)

VerifyPrompt is what verify asks; a pass is the token in the reply.

Variables

View Source
var (
	Gateways   = []string{"halo-proxy", "kong", policy.EngineExternal}
	Deliveries = []string{"devcontainer", "mdm", "halod"}
)

Company gateways and delivery channels.

View Source
var ErrForeign = errors.New("refusing to replace files Halos did not write")

ErrForeign is returned (wrapped) by Install when apply would replace files Halos did not write and replace was not set.

View Source
var ErrNotFound = errors.New("no halos.yaml")

ErrNotFound marks a missing policy repo.

View Source
var ErrPermission = errors.New("permission denied")

ErrPermission is returned (wrapped) when a managed path is not writable.

View Source
var FallbackTools = map[string]string{"claude-code": "2.1.280"}

FallbackTools is what a machine with no AI CLI on PATH starts with.

Functions

func AutoTools added in v0.5.0

func AutoTools(hs []Harness) (map[string]string, []string)

AutoTools is the default --tools: SuggestTools over the CLIs on PATH, or FallbackTools (with a warning) when that leaves nothing to pin.

func Company

func Company(o CompanyOptions) (map[string][]byte, error)

Company generates a policy repo for an organisation: halos.yaml, an optional Gateway overlay for the token helper, Helm values, the IdP client registration, enrollment config per delivery channel, a CI workflow, a smoke eval suite and a README of next steps. Keys are paths relative to the repo. Nothing here is secret: every credential is a reference to a Secret or env var. Everything but halos.yaml and gateway.yaml lives under .halos/: the policy loader reads every other YAML file in the repo as a policy document.

func DefaultRing

func DefaultRing(org *policy.Org) string

DefaultRing is the ring a single machine follows: the last (GA) ring.

func IsLoopback

func IsLoopback(u string) bool

IsLoopback reports whether u's host is localhost or a loopback IP.

func Issues

func Issues(is []policy.Issue) (out []string, ok bool)

Issues converts validation issues to plain strings; ok is false on any error.

func LocalProxy

func LocalProxy(org *policy.Org, absDir, listen string) (map[string][]byte, string, error)

LocalProxy returns the files for a single-developer halo-proxy on loopback (identity off: it listens on 127.0.0.1 only) keyed by path relative to the policy repo, plus the command that starts it. absDir is the policy repo's absolute path (halo-proxy resolves the snapshot path from it).

func Reachable

func Reachable(ctx context.Context, c *http.Client, base string) (int, error)

Reachable reports the HTTP status base answers with; any status counts (a gateway answers 401/404 without a token).

func SuggestProvider

func SuggestProvider(e Env) string

SuggestProvider picks the provider whose credential is already exported; anthropic when none is (ordering is the order checked).

func SuggestTools

func SuggestTools(hs []Harness) (tools map[string]string, warnings []string)

SuggestTools pins every installed CLI at the version found on PATH, so the first release changes no CLI binary. Empty when none is installed. A CLI whose version is not an exact semver (cursor-agent reports a date such as 2026.04.17-787b533) cannot be pinned by policy: it is left out and a warning says which tool and how to pin it, so one such CLI never fails onboarding.

Types

type Check

type Check struct {
	ID     string `json:"id"`
	Status string `json:"status"`
	Detail string `json:"detail"`
	Fix    string `json:"fix,omitempty"`
}

Check is one doctor finding. Fix is the exact command or step that resolves a warn or fail; it never contains a secret value.

type CompanyOptions

type CompanyOptions struct {
	Init        intent.InitOptions // org, tools, provider, models, gateway, issuer, client id, admins, presets
	GatewayKind string             // halo-proxy | kong | external (the company's own API gateway; Halos runs no data plane)
	AuthHelper  string             // command printing a short-lived gateway token on each laptop (optional)
	Delivery    []string           // devcontainer | mdm | halod
	Registry    string             // OCI repository releases are published to
	PolicyRepo  string             // git URL of this policy repo (Helm git-sync)
	PortalURL   string             // halo-server base URL
	HaloVersion string             // halo the CI workflow installs ("" or "dev" = latest)
}

CompanyOptions are an admin's interview answers.

type CompanyResult

type CompanyResult struct {
	DryRun bool              `json:"dryRun"`
	Dir    string            `json:"dir"`
	Files  map[string]string `json:"files"` // repo-relative path -> create | unchanged
	Valid  bool              `json:"valid"`
	Issues []string          `json:"issues"`
	Next   []string          `json:"next"`
}

CompanyResult is what `halo onboard company` decided per file.

func WriteCompany

func WriteCompany(dir string, o CompanyOptions, apply bool) (*CompanyResult, error)

WriteCompany generates the company repo into dir, validates it, and writes it when apply. It never overwrites a file that differs (exit with a list).

type Credential

type Credential struct {
	Env     string `json:"env"`
	For     string `json:"for"`
	Present bool   `json:"present"`
}

Credential reports whether a credential variable is set. Never its value.

func Credentials

func Credentials(e Env) []Credential

Credentials lists every known credential variable and whether it is set.

type Env

type Env struct {
	Run      Runner
	LookPath func(string) (string, error)
	Getenv   func(string) string
	GOOS     string
	HTTP     *http.Client // nil: skip network checks
	// ReadFile and Home read harness settings for the sandbox/egress posture
	// (ADR-0032); a nil ReadFile skips it, an empty Home skips per-user files.
	ReadFile func(string) ([]byte, error)
	Home     string
}

Env is everything onboarding reads from the machine, injectable for tests.

func System

func System() Env

System is the real machine.

type FilePlan

type FilePlan struct {
	Harness string `json:"harness"`
	Path    string `json:"path"` // the path the CLI reads
	Dest    string `json:"dest"` // where it is written (Path, or under --root)
	Mode    uint32 `json:"mode"`
	Action  string `json:"action"`
	Backup  string `json:"backup,omitempty"` // an existing differing file is kept here first
	Content string `json:"content"`
	// contains filtered or unexported fields
}

FilePlan is one managed config file: where it lands and what happens to it.

type Harness

type Harness struct {
	Name      string `json:"name"`
	Binary    string `json:"binary"`
	Installed bool   `json:"installed"`
	Path      string `json:"path,omitempty"`
	Version   string `json:"version,omitempty"`
	Error     string `json:"error,omitempty"`
}

Harness is one AI CLI as found on PATH.

func Detect

func Detect(ctx context.Context, e Env) []Harness

Detect probes every registered harness's binary with --version.

type InstallPlan

type InstallPlan struct {
	Ring     string            `json:"ring"`
	OS       string            `json:"os"`
	Root     string            `json:"root,omitempty"`
	Gateway  string            `json:"gateway,omitempty"`
	Versions map[string]string `json:"versions"` // harness -> pinned CLI version ("" = unpinned)
	Files    []FilePlan        `json:"files"`
	Warnings []string          `json:"warnings"`
}

InstallPlan is what installing a ring's release on this machine does.

func Install

func Install(dir, ring, goos, root string, apply, replace bool) (*InstallPlan, []string, error)

Install plans the managed-config install for the policy in dir and, when apply, writes it. A file Halos did not write (ActionForeign) blocks the whole apply unless replace is set. Idempotent: a second apply writes nothing.

func PlanInstall

func PlanInstall(org *policy.Org, ring, goos, root string) (*InstallPlan, error)

PlanInstall renders ring's release for goos and compares each file with what is on disk under root ("" = the real paths). It writes nothing.

func (*InstallPlan) Apply

func (p *InstallPlan) Apply() ([]string, error)

Apply writes every create/update/foreign file (callers gate foreign ones). A replaced foreign file is kept at <dest>.halos-backup (timestamped if that exists), and each write records its hash at <dest>.halos-sha256. Idempotent: a second Apply finds every file unchanged and writes nothing.

type LocalOptions

type LocalOptions struct {
	Dir   string
	Init  intent.InitOptions // Gateway "" = the local halo-proxy on loopback
	Ring  string             // "" = the GA (last) ring
	GOOS  string
	Root  string // stage managed config under this directory instead of the real paths
	Apply bool   // write halos.yaml (never the managed config: see Install)
	// Warnings are caller-side findings (autodetect left a CLI out) reported with the result.
	Warnings []string
}

LocalOptions drive the "my machine" path.

type LocalResult

type LocalResult struct {
	DryRun bool         `json:"dryRun"`
	Policy PolicyResult `json:"policy"`
	Valid  bool         `json:"valid"`
	Issues []string     `json:"issues"`
	// Warnings never fail the run: tools autodetect could not pin.
	Warnings []string     `json:"warnings"`
	Install  *InstallPlan `json:"install,omitempty"`
	Next     []string     `json:"next"`
}

LocalResult is one pass of init -> validate -> install plan.

func Local

func Local(o LocalOptions) (*LocalResult, error)

Local creates (or keeps) the policy in o.Dir, validates it as it would be written, and plans the managed-config install. It writes halos.yaml only when o.Apply and the policy validates; it never writes managed config. Idempotent: a second run reports the policy unchanged.

type PolicyResult

type PolicyResult struct {
	Path    string   `json:"path"`
	Action  string   `json:"action"`
	Content string   `json:"content"`
	Notes   []string `json:"notes"`
}

PolicyResult is what InitPolicy decided for halos.yaml.

func InitPolicy

func InitPolicy(dir string, o intent.InitOptions) (*intent.Change, PolicyResult, error)

InitPolicy is the idempotent front half of `halo init`: the halos.yaml o describes, as a change on dir. An existing halos.yaml is never replaced: the change is then empty and the result says whether it already matches. The caller validates (Change.Validate) and applies.

type ProxyResult

type ProxyResult struct {
	DryRun  bool              `json:"dryRun"`
	Files   map[string]string `json:"files"` // absolute path -> create | update | unchanged
	Command string            `json:"command"`
	Notes   []string          `json:"notes"`
}

ProxyResult is what `halo onboard proxy` decided.

func Proxy

func Proxy(e Env, dir, listen string, apply bool) (*ProxyResult, error)

Proxy writes (when apply) the local halo-proxy config into dir/.halos/local.

type Report

type Report struct {
	OK          bool         `json:"ok"`
	PolicyDir   string       `json:"policyDir"`
	Checks      []Check      `json:"checks"`
	Harnesses   []Harness    `json:"harnesses"`
	Credentials []Credential `json:"credentials"`
	// Posture is each installed harness's effective sandbox and egress
	// posture on this machine (ADR-0032).
	Posture []harness.Posture `json:"posture,omitempty"`
}

Report is the doctor result. OK is false when any check failed.

func Doctor

func Doctor(ctx context.Context, e Env, dir string) Report

Doctor checks the machine and the policy repo in dir ("" = skip policy).

type Runner

type Runner func(ctx context.Context, name string, args ...string) ([]byte, error)

Runner runs a program and returns its combined output.

type VerifyResult

type VerifyResult struct {
	Harness string   `json:"harness"`
	Command []string `json:"command"`
	Status  string   `json:"status"`
	Detail  string   `json:"detail"`
	Output  string   `json:"output,omitempty"`
}

VerifyResult is one headless round trip. Output is redacted and truncated.

func Verify

func Verify(ctx context.Context, e Env, h string, assumeAuth, dry bool, timeout time.Duration) VerifyResult

Verify runs h headless with VerifyPrompt (unless dry) and checks the reply. It is skipped when the CLI is missing or no credential variable is set and assumeAuth is false (a logged-in CLI needs --assume-auth: halo cannot see it).

type VerifySummary added in v0.5.0

type VerifySummary struct {
	Verified int    `json:"verified"`
	Failed   int    `json:"failed"`
	Skipped  int    `json:"skipped"`
	Total    int    `json:"total"`
	Text     string `json:"text"`
}

VerifySummary counts a batch of results: "verified X of N".

func Summarize added in v0.5.0

func Summarize(rs []VerifyResult) VerifySummary

Summarize counts rs. Dry-run results are in Total but are neither verified, failed nor skipped.

Jump to

Keyboard shortcuts

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