Documentation
¶
Overview ¶
Package vendoradmin renders one Halos policy into the settings of the vendors' cloud admin consoles (Claude for Teams/Enterprise, ChatGPT workspaces, Gemini for Google Cloud, GitHub Copilot, Cursor), reads the live settings where a vendor publishes a read API, diffs them, and applies the diff where it publishes a write API (ADR-0025). Everything else is exported as an artifact with manual steps.
Render is pure. Read and Apply go through a Client that takes a base URL, so tests run against httptest fakes. Credentials are never part of policy; errors are redacted of them.
Index ¶
- Constants
- Variables
- func CheckBaseURL(raw string) error
- func MCPTokenEnv(vendor string) string
- func MergeList(want []string, cur any) []any
- func Sanitize(s string) string
- func StatusCode(err error) int
- func Targets(va *policy.VendorAdmin) map[string]policy.VendorTarget
- func TokenEnv(vendor string) string
- func Vendors() []string
- type Access
- type Artifact
- type Change
- type Client
- func (c *Client) Apply(ctx context.Context, p *Plan) ([]Result, error)
- func (c *Client) Get(ctx context.Context, path string, out any, hdr ...string) (http.Header, error)
- func (c *Client) Read(ctx context.Context, p *Plan) (map[string]any, error)
- func (c *Client) Send(ctx context.Context, method, path string, body, out any) (http.Header, error)
- type Cred
- type Plan
- type Report
- type Result
- type Setting
Constants ¶
const ( GeminiBaseURL = "https://cloudaicompanion.googleapis.com" GitHubBaseURL = "https://api.github.com" )
Default API base URLs.
Variables ¶
var ErrStale = errors.New("the console changed since the plan was confirmed; nothing written, re-run `halo admin plan`")
ErrStale means the console changed between the confirmed plan and apply.
Functions ¶
func CheckBaseURL ¶
CheckBaseURL accepts an https API base URL, or http only to a loopback host (a local fake or proxy): a credential never leaves in clear text.
func MCPTokenEnv ¶
MCPTokenEnv is the environment variable the MCP admin_plan tool reads (only with `halo mcp serve --admin-read`). It differs from TokenEnv so a CLI admin token in a shell profile is never picked up by an agent's server.
func MergeList ¶
MergeList is want's strings (sorted, deduplicated) followed by the non-string entries of the live list cur, in their live order.
func Sanitize ¶
Sanitize drops control (Cc) and format (Cf) characters from s, so a policy or console value cannot move the cursor, recolour or hide text in a terminal (ESC sequences, CR) or reorder it (bidi overrides) in STEPS.md.
func StatusCode ¶ added in v0.4.0
StatusCode is the HTTP status of a vendor API error from a Client, or 0.
func Targets ¶
func Targets(va *policy.VendorAdmin) map[string]policy.VendorTarget
Targets maps each vendor name to its configured target.
Types ¶
type Change ¶
type Change struct {
Key string `json:"key"`
Current any `json:"current"` // nil = unset
Desired any `json:"desired"`
Access Access `json:"access"`
// Loosens is true when applying it would weaken a control the console
// has now. apply refuses these: a human makes that call in the console.
Loosens bool `json:"loosens,omitempty"`
}
Change is one setting whose live value differs from the desired one.
type Client ¶
type Client struct {
Vendor string
BaseURL string
Target policy.VendorTarget
// Token is the bearer credential. It is sent only in the Authorization
// header and scrubbed from every error.
Token string
HTTP *http.Client
}
Client reads and writes one vendor's console through its public API.
func NewClient ¶
func NewClient(vendor, base, token string, t policy.VendorTarget) *Client
NewClient returns the API client for vendor, or nil when it has no public read API (claude, codex, cursor: see ADR-0025). base "" uses the vendor's.
func (*Client) Apply ¶
Apply writes p's writable changes, p being the plan a human confirmed. It refuses the whole plan, sending no write, when a writable change loosens, a value is forbidden, a conflicting resource governs the target, or a fresh read of the console no longer matches the plan (ErrStale). It returns one Result per writable change it attempted, in order; a change after a failed write is not attempted.
func (*Client) Get ¶ added in v0.4.0
Get is an authenticated GET under the same rules as the console calls: the token only in the Authorization header and scrubbed from errors, no redirects, https (or loopback http) only, body capped at 1 MiB. The session ledger's vendor sources (internal/ledger) read audit APIs through it.
func (*Client) Read ¶
Read returns the live values of the settings p desires, keyed like Setting.Key, plus conflictsKey when other resources govern the target.
type Plan ¶
type Plan struct {
Vendor string `json:"vendor"`
Target string `json:"target"`
Desired []Setting `json:"desired"`
Changes []Change `json:"changes"`
Read bool `json:"read"` // the live state was read and diffed
// Conflicts are console resources Halos does not own that govern the same
// target (a non-halos Gemini setting bound to the project); apply refuses.
Conflicts []string `json:"conflicts,omitempty"`
Warnings []string `json:"warnings"`
Manual []string `json:"manual,omitempty"`
Artifacts []Artifact `json:"artifacts,omitempty"`
Error string `json:"error,omitempty"`
}
Plan is one vendor's desired state and, after Diff, its drift.
func RenderProfile ¶
func RenderProfile(vendor string, org *policy.Org, p *policy.Profile, t policy.VendorTarget) (*Plan, error)
RenderProfile renders one vendor for a profile (tests, the no-silent-drop check).
func (*Plan) CheckForbidden ¶
CheckForbidden refuses a plan that would emit a permission-bypass token (invariant 1; policy.BypassToken is the one shared check).
func (*Plan) Diff ¶
Diff fills p.Changes from current, the live values Client.Read returned (a missing key is unset). Manual settings are not diffed: nothing can read them. A list setting's live value is []any: Halos owns its string entries; any other entry (a Copilot {"if-any-match": [...]} rule) is preserved, so the desired value is the desired strings followed by those entries.
func (*Plan) Loosenings ¶
Loosenings are the changes apply refuses: the ones Halos would write. Loosening read-only settings are shown by plan but are a human's call.
type Report ¶
type Report struct {
Ring string `json:"ring"`
Profile string `json:"profile"`
Warnings []string `json:"warnings"`
Vendors []*Plan `json:"vendors"`
}
Report is every enabled vendor's plan for one policy.
func Render ¶
Render renders every vendor vendorAdmin enables from the profile of org.VendorAdminRing(). It reads nothing outside org. The org must validate.
func (*Report) Clients ¶
func (r *Report) Clients(targets map[string]policy.VendorTarget, creds map[string]Cred) (map[string]*Client, error)
Clients returns the API client for each vendor in r that has one, keyed by vendor. A base URL that is not https (or loopback http) is an error.
func (*Report) ReadLive ¶
func (r *Report) ReadLive(ctx context.Context, clients map[string]*Client, hint func(vendor string) string)
ReadLive reads and diffs every vendor that has an API client. A vendor that cannot be read gets Plan.Error (never a credential in it). hint names where the credential comes from, for the error.
type Result ¶
type Result struct {
Change Change `json:"change"`
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
}
Result is the outcome of one change Apply attempted.
type Setting ¶
type Setting struct {
Key string `json:"key"`
Value any `json:"value"`
From string `json:"from"` // the policy field it derives from
Access Access `json:"access"`
// Strict is the value that is the tighter control (a scalar setting);
// changing a setting away from it (or from a StrictAlso value, which the
// vendor treats the same) is a loosening. List settings loosen when an
// entry the console has is missing from the desired list.
Strict any `json:"-"`
StrictAlso []any `json:"-"`
// Sticky settings loosen whenever an existing value changes (a binding
// target: moving it un-governs the old target).
Sticky bool `json:"-"`
}
Setting is one desired console setting. Key is "<resource>.<field>" in the vendor's own naming, so a diff reads like the vendor's API.