Documentation
¶
Overview ¶
Package admincore holds the protocol-agnostic configuration operations outpost exposes — pairing, app CRUD, outbound mounts, built-in toggles, cluster kubeconfig, restart. Both the human-facing admin UI (HTTP + session cookie) and the agent-facing MCP server (HTTP + bearer token) dispatch into the same Server methods here, so validation rules and persistence semantics ship once.
What lives here vs. in the HTTP layer:
- admincore: validate input, mutate FileConfig under a shared mutex, update the live AppRegistry / OutboundManager, debounce restart.
- HTTP layer (adminui, mcpapi): authenticate the caller, parse the wire format, translate admincore errors into the protocol's status codes, render the response.
Errors returned by admincore are *APIError when callers need to map them to a transport-level status, plain errors when the operation was unable to even start. HTTP wrappers use RespondError to translate.
Index ¶
- func CloudboxHTTPBase(fc *conf.FileConfig) string
- func ValidateApp(ac *conf.AppConfig) error
- func ValidateOutbound(p *OutboundParams) error
- type APIError
- type AppUpsertParams
- type BuiltinView
- type BuiltinsParams
- type BuiltinsResult
- type ClusterView
- type Deps
- type KubeconfigParams
- type KubeconfigResult
- type LLMPoolStatusView
- type NetworkingParams
- type NetworkingResult
- type OutboundParams
- type OutboundSuggestion
- type PairParams
- type PairResult
- type SafeView
- type Server
- func (s *Server) AppSuggestions() ([]Suggestion, error)
- func (s *Server) ClearKubeconfig() (KubeconfigResult, error)
- func (s *Server) ConnectOutbound(path, password string) error
- func (s *Server) DeleteApp(name string) error
- func (s *Server) DeleteOutbound(path string) error
- func (s *Server) Deps() Deps
- func (s *Server) DisconnectOutbound(path string) error
- func (s *Server) ListApps() ([]conf.AppConfig, error)
- func (s *Server) ListOutbound() []agent.OutboundView
- func (s *Server) LoadConfig() (*conf.FileConfig, error)
- func (s *Server) OutboundSuggestions(ctx context.Context) ([]OutboundSuggestion, error)
- func (s *Server) Pair(ctx context.Context, p PairParams) (PairResult, error)
- func (s *Server) RotateProvisioningToken(name string) (string, error)
- func (s *Server) SafeView() (SafeView, error)
- func (s *Server) ScheduleRestart()
- func (s *Server) SetBuiltins(p BuiltinsParams) (BuiltinsResult, error)
- func (s *Server) SetCloudbox(base, accessToken, agentName string)
- func (s *Server) SetKubeconfig(p KubeconfigParams) (KubeconfigResult, error)
- func (s *Server) SetNetworking(p NetworkingParams) (NetworkingResult, error)
- func (s *Server) Status() (StatusView, error)
- func (s *Server) Unpair() (PairResult, error)
- func (s *Server) UpsertApp(p AppUpsertParams) (conf.AppConfig, error)
- func (s *Server) UpsertOutbound(p OutboundParams) error
- type StatusView
- type Suggestion
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CloudboxHTTPBase ¶
func CloudboxHTTPBase(fc *conf.FileConfig) string
CloudboxHTTPBase derives the HTTP(S) base URL of cloudbox from the matrix-tunnel pairing fields. Protocols are paired (wss↔https, websocket/ws/tcp↔http). Returns empty when the FileConfig isn't paired yet.
func ValidateApp ¶
ValidateApp normalizes ac in place (lowercasing scheme, defaulting host, trimming whitespace) and rejects invalid combinations. Returns *APIError so callers can map straight to a transport status code.
Same rules the admin SPA enforces client-side, replicated here as the authoritative gate.
func ValidateOutbound ¶
func ValidateOutbound(p *OutboundParams) error
ValidateOutbound trims, normalizes, and rejects bad combinations on p. After a successful call p.Scheme is one of "" (treated as "http"), "tcp", or "ssh"; required fields per-scheme are non-empty.
Types ¶
type APIError ¶
APIError carries an HTTP-style status alongside the human message so adminui can map it back to a gin status code and mcpapi can render an MCP-conformant error response.
func AsAPIError ¶
AsAPIError unwraps err into an *APIError if it is one (directly or via errors.As). Returns nil otherwise. HTTP layers call this to pick the right status code; plain (non-APIError) errors should be treated as 500.
func (*APIError) HTTPStatus ¶
HTTPStatus returns the suggested HTTP status code for this error.
type AppUpsertParams ¶
AppUpsertParams is the wire shape for adding or updating an app. The URL field is an alternative to the {Scheme, Host, Port, Socket} quartet — when non-empty, it is parsed via conf.AppTargetFromURL and wins over the split fields.
type BuiltinView ¶
type BuiltinView struct {
Enabled bool `json:"enabled"`
Available bool `json:"available"`
Target string `json:"target,omitempty"`
}
BuiltinView is the wire shape for one optional local-daemon proxy (podman/ollama). Enabled reflects the saved config; Available is the live detection result so the SPA can grey out the toggle when the daemon isn't running.
type BuiltinsParams ¶
type BuiltinsParams struct {
Shell *bool `json:"shell,omitempty"`
Desktop *bool `json:"desktop,omitempty"`
Clipboard *bool `json:"clipboard,omitempty"`
SSH *bool `json:"ssh,omitempty"`
SSHAllowLocalForward *bool `json:"ssh_allow_local_forward,omitempty"`
SSHAllowRemoteForward *bool `json:"ssh_allow_remote_forward,omitempty"`
SSHAllowAgentForward *bool `json:"ssh_allow_agent_forward,omitempty"`
SSHForwardSockets []string `json:"ssh_forward_sockets,omitempty"`
SFTP *bool `json:"sftp,omitempty"`
Podman *bool `json:"podman,omitempty"`
Ollama *bool `json:"ollama,omitempty"`
OllamaPool *bool `json:"ollama_pool,omitempty"`
Cluster *bool `json:"cluster,omitempty"`
// UpdateMode is one of "auto" / "manual" / "never" (see
// conf.UpdateMode* constants). Pointer-string so nil = "leave
// unchanged"; non-nil with an invalid value is rejected by
// SetBuiltins with a 400-class APIError.
UpdateMode *string `json:"update_mode,omitempty"`
}
BuiltinsParams is the partial-update shape for SetBuiltins. Pointer- bool fields mean "leave unchanged when nil"; non-nil fields are written through to the FileConfig.
The set of fields here is broader than the admin SPA currently surfaces — SSHAllowRemoteForward, SSHAllowAgentForward, and SSHForwardSockets exist in FileConfig but the SPA has no toggle for them. MCP / CLI callers can drive them directly.
type BuiltinsResult ¶
BuiltinsResult reports what happened. RestartPending is true when the change is one the tunnel / built-in routes need to reload to observe — callers should poll Status until the daemon is back.
type ClusterView ¶
type ClusterView struct {
Enabled bool `json:"enabled"`
APIURL string `json:"api_url,omitempty"`
NodeName string `json:"node_name,omitempty"`
HasToken bool `json:"has_token"`
HasCA bool `json:"has_ca"`
}
ClusterView is the redacted cluster status sent to UI / MCP callers. Token + CA bytes never leave the agent; presence is reported via has_token / has_ca.
type Deps ¶
type Deps struct {
// ConfigPath is where the persistent FileConfig lives. The Server
// serializes all read-modify-write sequences against ConfigPath
// under its own mutex.
ConfigPath string
// Apps is the live registry — admincore mutates it directly when
// the operator adds/removes/toggles custom apps. Concurrent-safe.
Apps *agent.AppRegistry
// Outbound manages local mount paths that proxy through cloudbox
// to remote outposts' apps. Optional — when nil the outbound
// operations report "not configured" rather than panic.
Outbound *agent.OutboundManager
// Restart, when set, is invoked (debounced) after a save that
// requires the tunnel or built-in routes to reload. Nil during
// tests; admincore short-circuits ScheduleRestart in that case.
Restart func()
// CloudboxBase + CloudboxAccessToken + AgentName feed the outbound-
// suggestions endpoint and the provisioning relay. CloudboxBase is
// empty until pairing completes; admincore returns a clear error
// instead of dialing nothing when the bearer is absent.
CloudboxBase string
CloudboxAccessToken string
AgentName string
// LLMPoolStatus, when set, returns the live pool diagnostic block
// rendered into SafeView. Nil when the pool service wasn't wired
// (Ollama off or daemon undetected). Closure rather than a concrete
// type so admincore doesn't import the ollama package.
LLMPoolStatus func() LLMPoolStatusView
}
Deps is what main.go threads into admincore.New. Everything here is concurrent-safe (or stateless): the Server doesn't own these values, it borrows them. AppRegistry and OutboundManager are live mutated across goroutines as the SPA / agent flips switches.
type KubeconfigParams ¶
type KubeconfigParams struct {
Kubeconfig string `json:"kubeconfig"`
NodeName string `json:"node_name,omitempty"`
Enable bool `json:"enable,omitempty"`
}
KubeconfigParams is the wire shape for SetKubeconfig.
- Kubeconfig: the pasted YAML (k3s.yaml for dev, cloudbox-issued kubeconfig for production).
- NodeName: optional override; empty defaults to AgentName at boot.
- Enable: when true, also flips Cluster.Enabled so the operator can paste + join in one action.
type KubeconfigResult ¶
type KubeconfigResult struct {
OK bool `json:"ok"`
Cluster ClusterView `json:"cluster"`
RestartPending bool `json:"restart_pending"`
}
KubeconfigResult reports the cluster view after the save plus whether the daemon will restart to pick up the join.
type LLMPoolStatusView ¶
type LLMPoolStatusView struct {
Enabled bool `json:"enabled"`
Running bool `json:"running"`
LastPushAt time.Time `json:"last_push_at,omitzero"`
LastModels int `json:"last_models"`
PushCount int64 `json:"push_count"`
LastError string `json:"last_error,omitempty"`
MaxParallel int `json:"max_parallel"`
InFlight int `json:"in_flight"`
CloudboxURL string `json:"cloudbox_url,omitempty"`
OllamaURL string `json:"ollama_url,omitempty"`
}
LLMPoolStatusView is the wire shape rendered into SafeView. Kept here (rather than in the ollama package) so the HTTP layers can read it without taking on an ollama dependency.
type NetworkingParams ¶
type NetworkingParams struct {
// LocalAddr — bind for the matrix-tunnel ingress. Empty to clear.
// Use *string so callers can distinguish "leave alone" (nil) from
// "clear to default" (pointer to "").
LocalAddr *string `json:"local_addr,omitempty"`
// VNCAddr — upstream for the /desktop bridge.
VNCAddr *string `json:"vnc_addr,omitempty"`
// AdminAddr — bind for the admin UI + MCP listener.
AdminAddr *string `json:"admin_addr,omitempty"`
// AdminUsers — when non-nil, replaces the entire allowlist. Pass
// an empty slice to revert to the legacy "anyone with the OS
// password is admin" mode.
AdminUsers *[]string `json:"admin_users,omitempty"`
}
NetworkingParams is the partial-update shape for SetNetworking. All fields are pointers / nil-able so the caller can change one knob without resetting the others. Pass an explicit empty string to clear a field (revert to env / hardcoded default).
type NetworkingResult ¶
NetworkingResult reports what changed. RestartPending is true whenever any field was modified — the listener bind addresses and the admin-users allowlist all take effect at boot only.
type OutboundParams ¶
type OutboundParams struct {
Path string `json:"path"`
Name string `json:"name"`
Host string `json:"host"`
User string `json:"user"`
Scheme string `json:"scheme,omitempty"`
LocalPort int `json:"local_port,omitempty"`
TTLSeconds int64 `json:"ttl_seconds,omitempty"`
}
OutboundParams mirrors the wire payload of POST /api/outbound. Lifted out of adminui so MCP tools can populate the same struct without reaching across packages.
type OutboundSuggestion ¶
type OutboundSuggestion struct {
Host string `json:"host"`
OsUser string `json:"os_user,omitempty"`
Name string `json:"name"`
Scheme string `json:"scheme,omitempty"`
RequireLogin bool `json:"require_login"`
IndexPath string `json:"index_path,omitempty"`
Title string `json:"title,omitempty"`
Online bool `json:"online"`
}
OutboundSuggestion is one row in the "Remote" dropdown — a host + an app on that host (or a synthetic SSH row pointing at the host's built-in /ssh endpoint).
type PairParams ¶
type PairParams struct {
Server string `json:"server,omitempty"`
Code string `json:"code"`
Name string `json:"name"`
Title string `json:"title,omitempty"`
AuthURL string `json:"auth_url,omitempty"`
ClientOnly bool `json:"client_only,omitempty"`
}
PairParams is the wire shape for the portal exchange.
- Server: portal URL (defaults to https://ai.dhnt.io when empty).
- Code: one-time pairing code from the portal (required).
- Name: host name to register (required).
- Title: optional human-readable subtitle shown in the portal.
- AuthURL: optional external app-level auth endpoint.
- ClientOnly: register as a credential-only outpost (no inbound listeners, no matrix tunnel) — see register --client-only.
type PairResult ¶
type PairResult struct {
OK bool `json:"ok"`
AgentName string `json:"agent_name"`
RestartPending bool `json:"restart_pending"`
}
PairResult reports the new AgentName cloudbox assigned (typically echoing the requested Name) plus the restart signal callers should poll on.
type SafeView ¶
type SafeView struct {
AgentName string `json:"agent_name"`
ServerAddr string `json:"server_addr"`
ServerPort int `json:"server_port"`
CloudboxURL string `json:"cloudbox_url,omitempty"`
Protocol string `json:"protocol,omitempty"`
RemotePort int `json:"remote_port"`
AuthURL string `json:"auth_url,omitempty"`
HasToken bool `json:"has_token"`
LocalAddr string `json:"local_addr,omitempty"`
VNCAddr string `json:"vnc_addr,omitempty"`
AdminAddr string `json:"admin_addr,omitempty"`
AdminUsers []string `json:"admin_users"`
Apps []conf.AppConfig `json:"apps"`
ShellEnabled bool `json:"shell_enabled"`
DesktopEnabled bool `json:"desktop_enabled"`
ClipboardEnabled bool `json:"clipboard_enabled"`
SSHEnabled bool `json:"ssh_enabled"`
SSHAllowLocalForward bool `json:"ssh_allow_local_forward"`
SSHAllowRemoteForward bool `json:"ssh_allow_remote_forward"`
SSHAllowAgentForward bool `json:"ssh_allow_agent_forward"`
SSHForwardSockets []string `json:"ssh_forward_sockets"`
SFTPEnabled bool `json:"sftp_enabled"`
ClientOnly bool `json:"client_only"`
Podman BuiltinView `json:"podman"`
Ollama BuiltinView `json:"ollama"`
OllamaPoolEnabled bool `json:"ollama_pool_enabled"`
UpdateMode string `json:"update_mode"`
LLMPool LLMPoolStatusView `json:"llm_pool"`
Cluster ClusterView `json:"cluster"`
Outbound []agent.OutboundView `json:"outbound"`
Defaults map[string]string `json:"defaults"`
}
SafeView is the redacted FileConfig sent over the API. Token never leaves the agent; presence is reported as has_token instead.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is the stateful object that the HTTP layers share. Holds the FileConfig serialization mutex and the restart-debounce timer so that adminui and mcpapi calling the same operations in quick succession (e.g. the SPA toggling several builtins) collapse into a single save dance and a single restart.
func New ¶
New constructs an admincore.Server. Deps.ConfigPath is required; other fields are optional (nil-checked at the call sites that need them).
func (*Server) AppSuggestions ¶
func (s *Server) AppSuggestions() ([]Suggestion, error)
AppSuggestions probes well-known socket paths and the local ycode manifest, returning the apps the user could enable with one click. Never mutates configuration.
func (*Server) ClearKubeconfig ¶
func (s *Server) ClearKubeconfig() (KubeconfigResult, error)
ClearKubeconfig removes the cluster credentials and the Enabled flag so a future boot doesn't try to dial a stale apiserver. Returns RestartPending=true when the cluster was previously joined (paired hosts) so callers can poll Status.
func (*Server) ConnectOutbound ¶
ConnectOutbound runs the cloudbox elevate flow for the named mount using the supplied OS password and starts the matrix_elev pinger. Returns 404 when the path is unknown.
func (*Server) DeleteApp ¶
DeleteApp removes an app by name from FileConfig and from the live AppRegistry. No-op when the name isn't registered (idempotent — the SPA's "remove" button doesn't care about prior state).
func (*Server) DeleteOutbound ¶
DeleteOutbound removes an outbound mount by path. Idempotent — no error when the path doesn't exist.
func (*Server) Deps ¶
Deps returns the underlying dependency struct (read-only access for HTTP layers that need e.g. AgentName or CloudboxBase).
func (*Server) DisconnectOutbound ¶
DisconnectOutbound drops the matrix_elev cookie for the named mount. Idempotent.
func (*Server) ListApps ¶
ListApps returns the apps slice from the on-disk FileConfig. Returns an empty slice (never nil) when no apps are registered, so JSON serialization stays a list.
func (*Server) ListOutbound ¶
func (s *Server) ListOutbound() []agent.OutboundView
ListOutbound returns the live state of every registered outbound mount. When no manager is wired (unpaired host), returns an empty slice instead of nil so JSON renders as a list.
func (*Server) LoadConfig ¶
func (s *Server) LoadConfig() (*conf.FileConfig, error)
LoadConfig is the exported read-only variant. HTTP layers use it for pure renders (GET /api/config, MCP resource reads) that don't need to hold the save mutex. Returns a copy view; mutators must go through the typed operations.
func (*Server) OutboundSuggestions ¶
func (s *Server) OutboundSuggestions(ctx context.Context) ([]OutboundSuggestion, error)
OutboundSuggestions calls cloudbox's /api/v1/hosts and flattens it into one row per (host, app), plus a synthetic SSH row per host whose built-in /ssh is mounted. Returns ServiceUnavailable when the outpost isn't paired yet (no AccessToken to authenticate with).
func (*Server) Pair ¶
func (s *Server) Pair(ctx context.Context, p PairParams) (PairResult, error)
Pair runs the portal exchange and merges the result into the persisted FileConfig (preserving locally-managed fields: Apps, Outbound, built-in toggles, Cluster). Schedules a restart so the new tunnel/identity takes effect.
func (*Server) RotateProvisioningToken ¶
RotateProvisioningToken mints a new 32-byte hex bearer for the named app and updates both the persisted FileConfig and the live registry. Errors with 404 when the app doesn't exist and 400 when TrustCloudIdentity is off (rotation is only meaningful when the relay is in use).
func (*Server) SafeView ¶
SafeView returns the redacted view of the on-disk FileConfig + live state (built-in availability probes, outbound mount status, pool diagnostic). The Token / AccessToken / ProvisioningToken values are NEVER included — presence is reported via has_token only.
func (*Server) ScheduleRestart ¶
func (s *Server) ScheduleRestart()
ScheduleRestart asynchronously triggers Deps.Restart after a short debounce so the in-flight HTTP response has time to flush AND so multiple back-to-back operations (the SPA auto-saves on every toggle) collapse into a single re-exec. Each call resets the timer.
func (*Server) SetBuiltins ¶
func (s *Server) SetBuiltins(p BuiltinsParams) (BuiltinsResult, error)
SetBuiltins applies the partial update p to the persisted FileConfig and (when the host is paired) schedules a restart so the new toggles take effect. On a first-time setup (AgentName empty) nothing is mounted yet, so the save is harmless and no restart is triggered.
func (*Server) SetCloudbox ¶
SetCloudbox updates the cloudbox base URL + access token + agent name after a re-pair (Pair mutates the FileConfig but the in-memory deps snapshot is stale until callers refresh it). HTTP layers call this after Pair returns successfully.
func (*Server) SetKubeconfig ¶
func (s *Server) SetKubeconfig(p KubeconfigParams) (KubeconfigResult, error)
SetKubeconfig parses a pasted kubeconfig, extracts the apiserver URL + bearer token + CA, and persists them into fc.Cluster. The kubeconfig itself is NOT stored — only the three fields the runner actually uses.
Triggers a restart when fc.Cluster.Enabled ends up true (joining the cluster on the next boot) and the host is paired.
func (*Server) SetNetworking ¶
func (s *Server) SetNetworking(p NetworkingParams) (NetworkingResult, error)
SetNetworking applies the partial update to the persisted FileConfig and (if anything changed and the host is paired) schedules a restart so the new listener bind / allowlist takes effect. First-time-setup hosts (AgentName empty) skip the restart — nothing is mounted yet, so a save is harmless.
func (*Server) Status ¶
func (s *Server) Status() (StatusView, error)
Status returns the lightweight paired-yet payload.
func (*Server) Unpair ¶
func (s *Server) Unpair() (PairResult, error)
Unpair clears the portal-controlled fields (AgentName, Token, etc.) while preserving locally-managed config (Apps, Outbound, builtins). Schedules a restart so the daemon drops its tunnel and reverts to the unpaired admin-UI-only mode.
New capability — the admin UI doesn't expose this today, but agents occasionally need to nuke a stale pairing without editing agent.json by hand.
func (*Server) UpsertApp ¶
func (s *Server) UpsertApp(p AppUpsertParams) (conf.AppConfig, error)
UpsertApp validates the params, persists the merged FileConfig, and mutates the live AppRegistry. No restart required — AppRegistry is concurrent-safe.
func (*Server) UpsertOutbound ¶
func (s *Server) UpsertOutbound(p OutboundParams) error
UpsertOutbound validates the params, refuses collisions with local app names and other listener-binding mounts, persists to FileConfig, and re-registers the live OutboundManager so the change takes effect without a restart.
type StatusView ¶
type StatusView struct {
Configured bool `json:"configured"`
AgentName string `json:"agent_name,omitempty"`
ServerAddr string `json:"server_addr,omitempty"`
CloudboxURL string `json:"cloudbox_url,omitempty"`
CurrentOSUser string `json:"current_os_user,omitempty"`
Build agent.BuildInfo `json:"build"`
BinaryPath string `json:"binary_path,omitempty"`
}
StatusView is the small "is outpost paired yet?" shape the SPA polls to decide what to render. Mirrors the legacy /api/status payload.
Build + BinaryPath are added so a remote operator (e.g. `outpost upgrade` running on another box that drives this daemon over MCP, or cloudbox's fleet view) can see the running daemon's provenance and the path of the binary to swap on disk.
type Suggestion ¶
type Suggestion struct {
Name string `json:"name"`
Scheme string `json:"scheme"`
Socket string `json:"socket,omitempty"`
Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"`
Role string `json:"role"`
Source string `json:"source"` // "wellKnown" | "ycodeManifest"
Note string `json:"note,omitempty"` // human-readable hint
Existing bool `json:"existing,omitempty"` // already registered with this name
}
Suggestion is one auto-detected local app the operator can register with a single click (in the admin UI) or a single MCP call.