agent

package
v0.14.3-dev Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 51 Imported by: 0

Documentation

Overview

Package agent runs on the home host, dials cloudbox over the matrix tunnel, and exposes local apps (ycode, shell, desktop, plus user-defined LAN services).

Index

Constants

View Source
const (
	BuiltinPodman = "podman"
	BuiltinOllama = "ollama"
	// BuiltinSandbox is the filtered container-sandbox mount. It speaks to
	// the same podman socket DetectPodman() finds (so availability is
	// gated on podman being installed), but registers a SEPARATE app whose
	// proxy is wrapped by the sandbox filter — distinct from the raw,
	// admin-only BuiltinPodman passthrough.
	BuiltinSandbox = "sandbox"
	// BuiltinFiles is the embedded File Browser mount — an in-process HTTP
	// handler (not an external daemon), the GUI sibling of /shell + /ssh
	// for remote view/download. Registered as a normal "http" app so it
	// flows through the existing per-app gate.
	BuiltinFiles = "files"
)

Builtin names — also the proxy slot names the admin UI surfaces and that get registered into the AppRegistry when enabled.

Variables

View Source
var HealthyProbe func() bool

HealthyProbe, when set by main.go, reports whether this binary is confirmed healthy — i.e. there is NO pending unconfirmed self-upgrade (the auto-rollback watchdog marker is absent). Left as a hook so the agent package doesn't import internal/agent/upgrade (which imports agent — an import cycle). nil → assume healthy.

Functions

func InitBootCount added in v0.9.0

func InitBootCount(dir string)

InitBootCount reads, increments, and persists the daemon boot counter at <dir>/boot_count, storing the new value for this process's BuildInfo reporting. Called once at daemon start. Best-effort: any IO error leaves bootCount at 0 (reported as "unknown") without failing boot.

func LoadOrCreateHostKey

func LoadOrCreateHostKey() (ssh.Signer, error)

LoadOrCreateHostKey returns outpost's persistent SSH host identity. On first call it generates an ed25519 keypair and writes it to <UserConfigDir>/matrix/ssh_host_ed25519 with mode 0600. Subsequent calls read the same file back.

The key lives in its own file (not in agent.json) so that re-pairing — which rewrites agent.json — does NOT regenerate the host identity. Clients that have cached our host key in known_hosts would otherwise see a REMOTE HOST IDENTIFICATION HAS CHANGED warning on every re-pair.

func RegisterProvisionRoutes

func RegisterProvisionRoutes(rg gin.IRouter, deps ProvisionDeps)

RegisterProvisionRoutes mounts the /_periscope/apps/:name/users surface on rg. Routes:

POST   /_periscope/apps/:name/users           upsert a grant
GET    /_periscope/apps/:name/users           list grants (30s cache)
DELETE /_periscope/apps/:name/users/:email    revoke a grant

Mounted on the main loopback listener — never advertised through the matrix tunnel. Authentication is per-app bearer (AppConfig. ProvisioningToken); outpost forwards to cloudbox's grant API using its own access_token. Outpost does not persist grant state — cloudbox is the source of truth, GET is opportunistically cached for 30 s.

func RegisterRoutes

func RegisterRoutes(rg *gin.RouterGroup, deps Deps)

RegisterRoutes attaches all matrix-agent routes onto rg. Always mounted at the root in the standalone binary; the routes are loopback-only and reached from cloudbox through the matrix tunnel.

func ServeLANListener added in v0.1.6

func ServeLANListener(ctx context.Context, ln net.Listener, deps sshHandlerDeps) error

ServeLANListener accepts plain TCP connections on `ln` and feeds each one to handleSSHConn as a NEW SSH session. Blocks until ln errors or ctx is canceled. Use as `errgroup.Go(func() error { return ServeLANListener(gctx, ln, deps) })`.

cloudboxVouched is always false here — LAN-direct callers haven't been vouched for by cloudbox, so the SSH PasswordCallback enforces the OS-password gate the same way it does for direct-loopback WS callers.

func ServeLANSSH added in v0.1.6

func ServeLANSSH(ctx context.Context, ln net.Listener, deps Deps) error

ServeLANSSH is the cmd/outpost-callable wrapper that builds the internal sshHandlerDeps from the public Deps shape and calls ServeLANListener. Lets the daemon main loop attach a LAN-direct SSH listener (FileConfig.SSHListenAddr) without main.go having to know the internal handler-deps fields.

Always passes cloudboxVouched=false (inside ServeLANListener): the LAN TCP path has no upstream vouching, so the OS-password gate applies.

func ServeLANSSHWS added in v0.5.0

func ServeLANSSHWS(ctx context.Context, ln net.Listener, deps Deps) error

ServeLANSSHWS mounts the same /ssh handler the loopback gin engine uses on a fresh HTTP server bound to ln. Replaces the plain-TCP ServeLANSSH path with a WS-mounted listener that accepts peer-ticket JWTs as the auth signal (the cookie itself never traverses the LAN).

`deps.SSHTicketPubkey` + `deps.SSHTicketVerifier` + `deps.SSHTicketAudience` are the new wiring. With them set, a client that presents `Authorization: Bearer <peer-ticket>` on the WS upgrade verifies without a password prompt. Empty pubkey or nil verifier disables the path — the handler still mounts, but every connection falls through to the OS-password gate (matching the legacy LAN-TCP behavior).

`X-Periscope-Role` is NOT trusted on this path (TrustPeriscopeRole=false) because a LAN listener that honored it would let any LAN device promote itself to admin by spoofing the header.

func StartEchoServer

func StartEchoServer(addr string) (net.Listener, error)

StartEchoServer starts a trivial TCP echo listener on addr (usually "127.0.0.1:0" so the kernel picks a port). Used by the matrix-tunnel round-trip integration test; not wired into the production agent.

Types

type AdminSet

type AdminSet map[string]struct{}

AdminSet is a case-insensitive set of OAuth-identified emails used by the OS-auth path to scope who is admin on this host. Empty set means the OS-auth default (admin on any OS-verified login) applies.

func NewAdminSet

func NewAdminSet(spec string) AdminSet

NewAdminSet parses a comma-separated email list into a set. Whitespace around each entry is trimmed and case is folded so that "Alice@Example.com" matches "alice@example.com".

func (AdminSet) Contains

func (s AdminSet) Contains(email string) bool

Contains reports whether email is in the admin set (case-insensitive).

type AppCapabilities

type AppCapabilities struct {
	Type string `json:"type"`
}

AppCapabilities is a free-form typed-app descriptor. Type is the only required field — it tells cloudbox "treat this app as a thing of class X." Currently the only recognized value is "llm" (for the built-in ollama proxy); cloudbox feature-detects unknown types so adding more later is backwards-compatible.

Pointer-shaped so a missing Capabilities serializes as null/omit rather than as an empty object.

type AppEntry

type AppEntry struct {
	Name         string `json:"name"`
	Scheme       string `json:"scheme,omitempty"`
	RequireLogin bool   `json:"require_login"`
	// ElevationRequired, when true, additionally demands the OS-password
	// (PAM) elevation at cloudbox — only meaningful when RequireLogin is
	// also true. Default false: a require_login app authenticates the
	// caller (owner or sharee) without forcing the owner through PAM.
	ElevationRequired bool             `json:"elevation_required,omitempty"`
	IndexPath         string           `json:"index_path,omitempty"`
	Capabilities      *AppCapabilities `json:"capabilities,omitempty"`
}

AppEntry is one declared app published to the cloud via GET /apps.

  • RequireLogin: when true, outpost (and cloudbox at the edge) require the caller to have proven local-OS auth before this app's tile/proxy is reachable. Replaces the legacy guest/user/ admin role tier.
  • Scheme: "http" for the reverse-proxy path or "tcp" for the WS↔TCP bridge; cloudbox uses it to know whether the local-side mount needs a subpath (http) or a TCP listener port (tcp).
  • IndexPath: optional landing sub-path the cloudbox SPA prepends when constructing this app's tile URL. Empty = "/". Enables the "virtual app" pattern (two AppConfig rows on the same upstream opening at different paths) without any proxy-side rewriting.
  • Capabilities: optional typed-app advertisement. Currently used for the built-in ollama proxy to surface {type:"llm"} so cloudbox can fold it into the model pool without a separate probe. Nil for everything else; omitted from JSON when nil (old cloudbox ignores the field).

type AppMeta

type AppMeta struct {
	RequireLogin bool
	// ElevationRequired demands OS-password (PAM) elevation in addition to
	// authentication. Only consulted when RequireLogin is true.
	ElevationRequired bool
	LANOnlyPaths      []string
	IndexPath         string
	// Capabilities is the optional typed-app advertisement (e.g.
	// {Type:"llm"} for the built-in ollama proxy). Nil for vanilla
	// HTTP apps.
	Capabilities *AppCapabilities
	// TrustCloudIdentity opts the app into the trusted-header SSO
	// contract — outpost stamps Remote-User / Remote-Email /
	// Remote-Groups (and passes through X-Periscope-User /
	// X-Periscope-Role) on requests that came through the matrix
	// tunnel. Off by default; the per-app sanitize pass strips any
	// inbound copies of these headers regardless.
	TrustCloudIdentity bool
	// SSOSecret is the HMAC key outpost signs the identity headers
	// with so the upstream app can verify the stamp came from outpost
	// (defends the LAN spoof window where an attacker could set
	// Remote-User on a request that bypasses outpost). Empty means no
	// signature is stamped — upstream falls back to its own login.
	SSOSecret string
}

AppMeta carries the access-control + display fields the registry associates with each app. Internal to register/registerTCP. Tests and the simple Register helper pass a zero-value or a partial value.

type AppRegistry

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

AppRegistry maps app names (e.g. "ycode") to the local URL they live at (e.g. "http://127.0.0.1:8765"). It is loopback-only by design: the agent itself is only reachable through the matrix tunnel, and the agent only forwards under tier-1 trust (set by the cloud server in the X-Periscope-User header).

Socket-backed entries (scheme=unix|npipe) use a per-app Transport whose DialContext dials the local socket; the URL host is a synthetic "socket" placeholder that the upstream daemon ignores.

func NewAppRegistry

func NewAppRegistry() *AppRegistry

func (*AppRegistry) AddIntercept

func (r *AppRegistry) AddIntercept(name, prefix string, h http.Handler)

AddIntercept binds prefix → h under app `name`. Subsequent requests to /app/<name>/<rest> whose rest matches prefix (segment-anchored) are handled by h instead of forwarded to the reverse proxy. Multiple intercepts may be registered on the same app; matching is in registration order, longest prefix wins for equal-tied entries.

No-op when name is unknown — the caller might decorate before the app's main proxy is registered; the metadata sticks until somebody queries Entries() (which doesn't care about intercepts) or the user calls Unregister (which wipes everything).

func (*AppRegistry) Entries

func (r *AppRegistry) Entries() []AppEntry

Entries returns the registered apps' metadata for the GET /apps publish. TCP apps are listed alongside HTTP ones; the cloud uses `scheme` to pick the right tile shape.

func (*AppRegistry) LookupByProvisioningToken

func (r *AppRegistry) LookupByProvisioningToken(token string) (string, bool)

LookupByProvisioningToken returns the name of the app whose ProvisioningToken matches the given bearer, or ("", false) on miss. O(n) over registered apps, which is acceptable — provisioning is a low-volume operation (user grants change infrequently).

func (*AppRegistry) LookupTCP

func (r *AppRegistry) LookupTCP(name string) string

LookupTCP returns the registered TCP target "host:port" (or "" when name is not a TCP-mode app).

func (*AppRegistry) LookupTarget

func (r *AppRegistry) LookupTarget(name string) *url.URL

LookupTarget returns the registered HTTP target URL (or nil). TCP-mode apps have no URL; use LookupTCP for those.

func (*AppRegistry) Names

func (r *AppRegistry) Names() []string

Names returns registered app names (HTTP and TCP combined).

func (*AppRegistry) ProvisioningToken

func (r *AppRegistry) ProvisioningToken(name string) string

ProvisioningToken returns the bearer associated with name, or "" if none is set. Used by the admin UI to surface the token to the operator.

func (*AppRegistry) ProxyTo

func (r *AppRegistry) ProxyTo(c *gin.Context, name, rest string)

ProxyTo is the gin-param-free entry point used by both the standard /app/:name/*p route and the admin UI's `/<name>/*` local-access route (so users can hit http://localhost:17777/ollama/... directly without going through the cloudbox tunnel). Callers pass the captured wildcard `rest` as the upstream path to forward (leading slash included; an empty value is treated as "/").

For TCP-mode apps (ssh, postgres, …) the same route accepts a WebSocket upgrade and byte-bridges to the registered host:port. The `rest` argument is ignored — TCP has no notion of a sub-path.

func (*AppRegistry) Register

func (r *AppRegistry) Register(name, target string) error

Register adds (or replaces) an app entry with the default access-control posture (RequireLogin=true, no LAN-only paths, no IndexPath). target must be an absolute URL. Used by tests and by callers that don't have an AppConfig handy.

func (*AppRegistry) RegisterFromConfig

func (r *AppRegistry) RegisterFromConfig(ac conf.AppConfig) error

RegisterFromConfig is a convenience that builds a target from an AppConfig and registers it. Disabled entries are skipped (so the admin UI can keep them around without proxying them). Socket-backed apps (scheme=unix|npipe) get a custom Transport that dials the socket.

func (*AppRegistry) RegisterWithMeta

func (r *AppRegistry) RegisterWithMeta(name, target string, meta AppMeta) error

RegisterWithMeta is Register with explicit per-app metadata. Used by built-in apps and tests that need to assert specific gating.

func (*AppRegistry) SSOSecret added in v0.4.0

func (r *AppRegistry) SSOSecret(name string) string

SSOSecret returns the HMAC key associated with name, or "" if none is set. Used by `outpost apps secret <name>` and the admin UI to surface the value to the operator.

func (*AppRegistry) SetCapabilities

func (r *AppRegistry) SetCapabilities(name string, caps *AppCapabilities)

SetCapabilities attaches (or clears, when caps is nil) a typed-app descriptor to an already-registered app. The capabilities-via-AppMeta path is for callers that construct the meta themselves; this helper is for the boot path in main.go, where built-ins register via RegisterFromConfig (which doesn't carry capability info) and then the caller decorates by name. No-op when name is unknown — we don't want a typo at boot to crash the agent.

func (*AppRegistry) SetProvisioningToken

func (r *AppRegistry) SetProvisioningToken(name, token string)

SetProvisioningToken records or clears the bearer that the user-sync relay endpoint (/_periscope/apps/<name>/users) accepts as the caller's proof of identity. Empty token clears the entry. Safe to call on an unknown name (token is stored regardless; if the app is later registered with the same name, lookups will find it).

func (*AppRegistry) SetProxyWrap

func (r *AppRegistry) SetProxyWrap(name string, wrap func(http.Handler) http.Handler)

SetProxyWrap attaches a middleware applied to the reverse-proxy handler when /app/<name>/<rest> proxies a non-intercept request. Passing nil clears any existing wrapper. Useful for instrumentation that needs to wrap the proxy itself (e.g. in-flight counters) rather than serve a sub-path.

func (*AppRegistry) SetSSOSecret added in v0.4.0

func (r *AppRegistry) SetSSOSecret(name, secret string)

SetSSOSecret records or clears the HMAC key used to sign identity headers stamped on requests proxied to this app. Empty clears the entry. Read per-request inside the proxy Rewrite callback; safe to call on an unknown name.

func (*AppRegistry) Unregister

func (r *AppRegistry) Unregister(name string)

Unregister removes an app entry. No-op if the name is not registered.

type AuthRequest

type AuthRequest struct {
	User     string `json:"user"`
	Password string `json:"password" binding:"required"`
}

AuthRequest is the body of POST /auth.

User is consulted on both code paths:

  • OS path: must match the agent's running OS user; without that match PAM/dscl/LogonUserW would need root to verify a different account and we'd be silently weakening the gate.
  • AuthURL path: forwarded verbatim to the external endpoint, which owns the application-level user list.

type AuthResponse

type AuthResponse struct {
	User string `json:"user"`
	Role string `json:"role"` // "admin" or "user"
}

AuthResponse is the body of a successful POST /auth.

type BuildInfo

type BuildInfo struct {
	Version   string `json:"version,omitempty"`  // semver tag, e.g. "v0.2.0"; empty for untagged builds
	Commit    string `json:"commit"`             // full git sha1, empty if no VCS info
	VCSTime   string `json:"vcs_time,omitempty"` // ISO-8601 commit timestamp
	Dirty     bool   `json:"dirty"`              // true if working tree had uncommitted changes at build
	GoVersion string `json:"go_version"`         // e.g. "go1.26.0"
	OS        string `json:"os,omitempty"`       // runtime.GOOS — "darwin" / "linux" / "windows" (compile target)
	Arch      string `json:"arch,omitempty"`     // runtime.GOARCH — "arm64" / "amd64"

	// OSVersion is the actual host OS at RUNTIME (sw_vers / /etc/
	// os-release / cmd ver), e.g. "macOS 15.1.0" / "Ubuntu 24.04
	// LTS". Distinct from OS above which is the binary's compile
	// target — they typically match but can disagree if a binary
	// is shipped cross-OS (mostly an alert that something's
	// misconfigured).
	OSVersion string `json:"os_version,omitempty"`

	// BinarySize is the on-disk size of os.Executable() in bytes.
	// Useful for the SPA host row's at-a-glance "is the binary the
	// expected ballpark size or is something truncated."
	BinarySize int64 `json:"binary_size,omitempty"`

	// InstalledAt is the mtime of os.Executable() — when the binary
	// file was last written to disk. Reflects the most recent
	// upgrade/install (the daemon's previous swap or the operator's
	// scp), NOT the daemon's process start.
	InstalledAt time.Time `json:"installed_at,omitempty"`

	// DaemonStartedAt is the process-start timestamp captured at
	// the first ReadBuildInfo call via the package-level var. Stable
	// for the life of this daemon.
	DaemonStartedAt time.Time `json:"daemon_started_at,omitempty"`

	// BootCount is the persisted monotonic daemon-start counter. The
	// fleet health-gate diffs it between polls to detect a crash-loop
	// (a jump > 1 inside a rollout bake window). Omitted when 0 / not
	// yet initialized — cloudbox treats absent as unknown.
	BootCount int `json:"boot_count,omitempty"`

	// Healthy reports whether this binary is confirmed healthy (no
	// pending unconfirmed self-upgrade). Always emitted (no omitempty)
	// so a genuine false is visible to cloudbox; defaults true when no
	// HealthyProbe is wired (unpaired / pre-upgrade hosts).
	Healthy bool `json:"healthy"`
}

BuildInfo describes the provenance of this outpost binary. Sourced from runtime/debug.ReadBuildInfo() (commit, vcs_time, dirty, go_version are stamped automatically by `go build` in a VCS checkout), plus Version which is ldflags-injected at release-tag build time, plus OS/Arch from runtime.GOOS/GOARCH so cloudbox knows which platform artifact to push when fan-out-rolling the fleet.

Consumed by GET /version (full JSON) and embedded as a short string in GET /apps so cloudbox can surface "is this outpost up to date" without a coordinated cloudbox change.

func ReadBuildInfo

func ReadBuildInfo() BuildInfo

ReadBuildInfo returns the build metadata embedded in the running binary. Returns zero values for the VCS fields if debug.ReadBuildInfo fails (which it doesn't for any normal `go build`-produced binary).

func (BuildInfo) Short

func (b BuildInfo) Short() string

Short returns a one-line human-readable identifier. Prefers the semver tag when present (e.g. "v0.2.0"); falls back to the 7-char commit with a "-dirty" suffix when applicable; "unknown" when the binary was built without VCS info (e.g. via `go run` or with -buildvcs=false). Suitable for embedding in /apps for cloudbox.

func (BuildInfo) ShortCommit added in v0.7.1

func (b BuildInfo) ShortCommit() string

ShortCommit returns the 7-char commit, or "" when the binary carries no VCS info. Unlike Short() it never substitutes the release tag — Short() is for human display; this is the value to compare against an upgrade envelope's commit field. (On release builds Short() returns "v0.7.0"-style tags, which can never match a sha — the mixup that let the v0.7.0 fleet fan-out re-apply on the canary host and overwrite its rollback copy.)

type BuiltinDetector

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

BuiltinDetector caches DetectPodman/DetectOllama for a short TTL so repeated admin-UI calls don't probe the sockets on every request.

func NewBuiltinDetector

func NewBuiltinDetector(ttl time.Duration) *BuiltinDetector

NewBuiltinDetector returns a detector with the given probe-result TTL. Pass 0 to disable caching (mostly for tests).

func (*BuiltinDetector) Ollama

func (d *BuiltinDetector) Ollama() BuiltinTarget

Ollama returns the cached or freshly-probed ollama target.

func (*BuiltinDetector) Podman

func (d *BuiltinDetector) Podman() BuiltinTarget

Podman returns the cached or freshly-probed podman target.

type BuiltinTarget

type BuiltinTarget struct {
	Name      string
	Scheme    string
	Socket    string // when Scheme == "unix"
	URL       string // when Scheme == "http" — full base URL, e.g. http://127.0.0.1:11434
	Available bool
}

BuiltinTarget describes one of the optional local-daemon proxies (podman, ollama). Available reports whether the daemon is reachable on the suggested socket/URL right now; the admin UI uses this to grey out toggles for daemons that aren't installed. Scheme is "unix" for socket targets and "http" for HTTP base-URL targets.

func DetectOllama

func DetectOllama() BuiltinTarget

DetectOllama probes the local Ollama HTTP endpoint. Ollama doesn't publish a health endpoint, so we just check that something HTTP-shaped is listening — the daemon answers any path with at least an HTTP status line.

Honors $OLLAMA_HOST when set (Ollama's own env-var contract — users who run the daemon on a non-default port set this and expect every tool in the ecosystem to follow). Accepts both bare "host:port" and full "http(s)://host:port" forms. Falls back to the default loopback URL when unset.

func DetectPodman

func DetectPodman() BuiltinTarget

DetectPodman probes the usual podman socket paths and returns a description suitable both for registering as an app and for grey-out rendering in the admin UI. The first reachable socket wins. When none are reachable, Socket is still populated with the first candidate so the UI can surface "tried <path>".

type Deps

type Deps struct {
	AgentName string
	Apps      *AppRegistry
	Auth      hostauth.Authenticator
	Admins    AdminSet
	AuthURL   string
	VNCAddr   string // default 127.0.0.1:5900

	// Built-in route toggles. Zero value = enabled, so callers that don't
	// care about toggling keep the old default-on behavior.
	ShellDisabled     bool
	DesktopDisabled   bool
	ClipboardDisabled bool
	SSHDisabled       bool

	// SSHAllowLocalForward gates whether the SSH server accepts
	// `direct-tcpip` channels (stock `ssh -L` / `ssh -D`). Zero value
	// (false) means rejected — callers must opt in. main.go threads
	// `fc.SSHAllowLocalForwardOn()` here, which defaults to true on
	// fresh + legacy configs.
	SSHAllowLocalForward bool

	// SSHAllowRemoteForward gates whether the SSH server honors
	// `tcpip-forward` global requests (stock `ssh -R`). Same opt-in /
	// loopback-bind story as SSHAllowLocalForward.
	SSHAllowRemoteForward bool

	// SSHAllowAgentForward gates whether the SSH server accepts
	// `auth-agent-req@openssh.com` channel-requests (stock `ssh -A`).
	// Default off here; main.go threads `fc.SSHAllowAgentForwardOn()`
	// which is default-on. Per-session Unix socket lives in a 0700
	// tempdir, set as SSH_AUTH_SOCK in the runner env.
	SSHAllowAgentForward bool

	// SFTPEnabled gates whether the SSH server accepts the "sftp"
	// subsystem request — required for modern openssh `scp` (8.8+) and
	// for `sftp` itself. Zero value (false) means rejected; callers must
	// opt in. Disabling forces clients to use legacy `scp -O` (the exec
	// channel) which is also supported but slower.
	SFTPEnabled bool

	// SSHHostKey is the persistent host identity for the embedded SSH
	// server reached at /ssh. Nil means /ssh will not mount even if
	// SSHDisabled is false — callers pass a key loaded via
	// LoadOrCreateHostKey() at boot.
	SSHHostKey ssh.Signer

	// PeerHosts widens the SSH `direct-tcpip` destination allowlist to
	// any hostname registered as a paired outpost in this cloudbox
	// account, on top of the always-allowed loopback set. Nil → only
	// loopback destinations (the pre-existing posture). Constructed in
	// main.go from fc.AccessToken + cloudbox endpoint, so unpaired
	// outposts pass nil and keep the tight default.
	PeerHosts *peerhosts.Registry

	// SSHForwardSockets extends the unix-socket allowlist for
	// `direct-streamlocal@openssh.com` channel-opens — the primitive
	// behind `podman --connection=<host>`. The built-in defaults
	// (podman + canonical docker sockets) always apply on top of this
	// list; entries here are exact-matched after filepath.Clean.
	SSHForwardSockets []string

	// SelfName is the agent's own AgentName, forwarded to the SSH
	// handler so peer-tunneled dials can stamp X-Outpost-Peer-Origin
	// for cloudbox's audit log. Empty string is harmless — cloudbox
	// just records "unknown" as the origin.
	SelfName string

	// CloudboxBase + CloudboxProtocol + AccessToken jointly enable the
	// peer-tunneled direct-tcpip path (`ssh -J peerA peerB`). When set
	// and the dial target is a paired peer (not loopback) on port 22,
	// the SSH server routes the bytes through cloudbox's
	// /h/<peerB>/ssh WSS endpoint instead of attempting a LAN net.Dial
	// that would usually fail on DNS. Empty fields keep the dial path
	// loopback-only-plus-LAN-DNS.
	CloudboxBase     string
	CloudboxProtocol string
	AccessToken      string

	// SSHTicketPubkey, SSHTicketVerifier, and SSHTicketAudience
	// configure the peer-ticket auth path for the LAN-WS SSH
	// listener. Threaded from cmd/outpost/main.go where the
	// FileConfig and AgentName are known. nil/empty disables the
	// path: an unpaired outpost or one without a configured pubkey
	// can still serve a LAN-WS listener, it just won't accept any
	// connections passwordlessly.
	SSHTicketPubkey   ed25519.PublicKey
	SSHTicketVerifier *peerticket.Verifier
	SSHTicketAudience string

	// MountUpgradeRoute, if non-nil, is invoked once during
	// RegisterRoutes with the root gin.RouterGroup so an external
	// package can attach POST /admin/upgrade. Decoupled this way to
	// avoid an import cycle: the upgrade package imports agent for
	// BuildInfo, so agent can't import upgrade directly. The route
	// itself relies on tunnel-as-auth-boundary (same as /apps); no
	// bearer is required at the HTTP layer.
	MountUpgradeRoute func(rg *gin.RouterGroup)

	// MountWarmRoute, if non-nil, is invoked during RegisterRoutes with
	// the root group so the warm-serving package can attach
	// POST /admin/warm (cloudbox-driven load/shard/unload of warm
	// models). Same trust model as MountUpgradeRoute — tunnel-as-auth-
	// boundary, no bearer at the HTTP layer — and mounted only on paired
	// hosts. Decoupled as a closure so agent doesn't import the warm
	// package.
	MountWarmRoute func(rg *gin.RouterGroup)

	// MountRepairRoute, if non-nil, is invoked during RegisterRoutes with
	// the root group so the CI-repair package can attach POST /admin/repair
	// (cloudbox-driven trigger to start a band-escalating self-fix on this
	// host). Same trust model as MountWarmRoute — tunnel-as-auth-boundary,
	// no bearer at the HTTP layer — and mounted only on paired hosts.
	// Decoupled as a closure so agent doesn't import the repair package.
	MountRepairRoute func(rg *gin.RouterGroup)

	// UpdateMode is the closure /apps calls to surface the current
	// host's update policy (auto/manual/never). Reported alongside
	// version/os/arch so cloudbox's SPA can render the right badge
	// variant per host (e.g. "Pending — Apply" only for manual).
	// nil → field omitted from the envelope; cloudbox treats absent
	// as legacy / "auto".
	UpdateMode UpdateModeProvider

	// SystemInfo is the closure /apps calls to surface this host's
	// capability snapshot (cpu count, memory, disk, arch, model).
	// Cloudbox stores the latest reading on HostEntry and renders
	// it in the cluster view; the data also feeds future LB
	// heuristics (e.g. prefer hosts with more memory for
	// memory-heavy workloads). nil → field omitted; cloudbox
	// shows "—" for unknown.
	SystemInfo func() any

	// ClusterInfo, when non-nil, contributes a `cluster` block to
	// the /apps poll response. Cloudbox's host_poller consumes it
	// to reconcile the per-host kubelet_port in the hosts table
	// (a desync the outpost is the source of truth for — the port
	// is what its k3s-agent --kubelet-arg=port + in-container frpc
	// publish). Returning an empty map is fine; cloudbox treats zero
	// KubeletProxyPort as "no port allocated."
	ClusterInfo func() any
}

Deps is what `matrix-agent` (or a future host application) supplies to the agent's local HTTP routes.

AuthURL switches /auth between two strategies:

  • empty → host-OS path. Submitted username must match the agent's own OS user; password is verified via hostauth (PAM / dscl / LogonUserW). Role defaults to admin; Admins, when non-empty, downgrades emails not in the list to user.
  • set → delegate to an external endpoint. The agent POSTs {user,password} and trusts the returned {user,role}. Admins is ignored on this path.

type OutboundManager

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

OutboundManager registers local-mount → remote-outpost-app mappings and drives the per-mount connection state.

Lifecycle of one mount:

  Register      Connect(pw)           Disconnect / pinger-failure
cfg only ── elev cookie + pinger ── back to cfg-only

Connect mints a matrix_elev cookie via cloudbox's per-app or per-builtin elevate endpoint (Bearer access_token + {user, password}):

  • http / tcp scheme → POST /h/<host>/app/<name>/elevate
  • ssh scheme → POST /h/<host>/ssh/elevate

The cookie's Path narrows to the specific (host, app|builtin), so an elevation cannot be replayed against a sibling app. Starts a 4-minute pinger to slide the idle TTL. Disconnect (or a pinger failure indicating absolute expiry) drops the cookie; the operator must Connect again. Cookies are NEVER persisted to disk — only the OutboundConfig is.

func NewOutboundManager

func NewOutboundManager(serverURL, accessToken string, client *http.Client) *OutboundManager

NewOutboundManager builds a manager with the given cloudbox base URL and bearer access_token. serverURL is trimmed of any trailing slash. Pass an explicit *http.Client to override the timeout policy in tests; nil uses the package default.

func (*OutboundManager) AutoReconnect

func (m *OutboundManager) AutoReconnect()

AutoReconnect rehydrates persisted matrix_elev cookies for every registered mount and spawns the pinger + (for tcp/ssh-scheme mounts) the loopback listener. Equivalent to calling Connect for each mount, but without the password / cloudbox round-trip — the cookie was minted in a previous outpost lifetime and is good until cloudbox's pinger 4xx tears it down.

Call once at startup, AFTER Register. Subsequent Register calls don't re-trigger AutoReconnect: those are operator-initiated config edits where the right semantic is "respect the new state exactly" (and Register already wipes stale cookies for changed rows).

Listener bind failures (port already in use, permission denied) are logged per-mount and the affected mount is left in cfg-only state; the operator can resolve the conflict and Connect manually. Successful mounts are not blocked by a failure on a sibling — each mount is independent.

func (*OutboundManager) Connect

func (m *OutboundManager) Connect(path, password string) error

Connect authenticates to the remote host via cloudbox's elevate flow and stores the resulting matrix_elev cookie in memory. Starts a pinger goroutine to slide the idle TTL.

func (*OutboundManager) Disconnect

func (m *OutboundManager) Disconnect(path string)

Disconnect drops the in-memory cookie for path, stops the pinger, removes the persisted cookie file, and — for tcp mounts — closes the loopback listener. No server-side revocation: cloudbox's matrix_elev is a stateless JWT.

func (*OutboundManager) Has

func (m *OutboundManager) Has(path string) bool

Has reports whether path is currently registered.

func (*OutboundManager) List

func (m *OutboundManager) List() []OutboundView

List returns one OutboundView per registered config, sorted by path.

func (*OutboundManager) ProxyTo

func (m *OutboundManager) ProxyTo(c *gin.Context, path, rest string)

ProxyTo is the request handler. It forwards an inbound HTTP request (already stripped of the leading /<path>/ prefix into `rest`) through cloudbox to the remote outpost's registered app. Streaming responses (Ollama's /api/generate, etc.) flow through because we copy resp.Body to the gin writer with io.Copy and never buffer the full body.

func (*OutboundManager) Register

func (m *OutboundManager) Register(cfgs []conf.OutboundConfig)

Register replaces the registered config set with cfgs. Mounts that disappeared get their pinger torn down. A surviving mount keeps its live connection only when its cfg is byte-identical to the previous one — any change to scheme/local_port/name/host/user invalidates the existing conn (in particular, a stale TCP listener on the old port must be closed before we'd be willing to bind a new one).

func (*OutboundManager) Stop

func (m *OutboundManager) Stop()

Stop cancels every pinger and closes any tcp listeners. Call on process shutdown so goroutines exit cleanly. Configs and the in-memory cookie map are NOT cleared; another process boot will reload configs from disk (cookies are not persisted).

type OutboundView

type OutboundView struct {
	Path        string `json:"path"`
	Name        string `json:"name"`
	Host        string `json:"host"`
	User        string `json:"user"`
	Scheme      string `json:"scheme"`
	LocalPort   int    `json:"local_port,omitempty"`
	TTLSeconds  int64  `json:"ttl_seconds,omitempty"`
	Connected   bool   `json:"connected"`
	ConnectedAt string `json:"connected_at,omitempty"`
}

OutboundView is the API shape the admin UI consumes — config + status.

type ProvisionDeps

type ProvisionDeps struct {
	Apps         *AppRegistry
	HTTPClient   *http.Client
	CloudboxBase string
	AccessToken  string
	AgentName    string
}

ProvisionDeps wires the relay endpoint that lets a cooperating app push its user grants up to cloudbox via outpost. The pieces are supplied at boot from main.go:

  • Apps holds the live registry whose per-app ProvisioningToken authenticates the caller.
  • HTTPClient is the http.Client used to talk to cloudbox. Nil means "use a default with a 30 s timeout". Tests inject a client with a RoundTripper that points at a httptest.NewServer.
  • CloudboxBase is the cloudbox HTTP(S) base URL (e.g. https://ai.dhnt.io). Empty means outpost is unpaired and the handler refuses with 503 — no cloudbox to forward to.
  • AccessToken is outpost's bearer credential to cloudbox. Empty means unpaired, same 503 path as CloudboxBase.
  • AgentName is the host identity cloudbox knows this outpost by, used in the cloudbox URL path (/api/hosts/<host>/apps/<name>/grants).

type STCPVisitor added in v0.1.0

type STCPVisitor struct {
	Name       string
	ServerUser string
	ServerName string
	Secret     string
	BindAddr   string
	BindPort   int
}

STCPVisitor declares a "secret TCP" visitor — frp's reverse-direction primitive that opens a local listener and tunnels each accepted conn through the matrix-tunnel server to a service some OTHER frp client has published as an STCP proxy. Cloudbox uses this to expose its embedded apiserver to outposts (see hub/internal/tunnel/serverdial.go on the publisher side) so `k3s agent` can dial https://127.0.0.1:PORT from this outpost.

  • ServerUser : the User the publisher registered under (cloudbox publishes as "cloudbox"; outposts pass that here).
  • ServerName : the Name of the published proxy (e.g. "k3s-apiserver").
  • Secret : shared-secret authenticating the visitor to the publisher. Distinct from TunnelConfig.Token, which only gates entry to the tunnel itself.
  • BindAddr / BindPort : where to expose the visitor's local listener for in-process clients on this outpost. 127.0.0.1 by default.

type TCPProxy

type TCPProxy struct {
	Name       string
	LocalIP    string
	LocalPort  int
	RemotePort int
}

TCPProxy declares one local TCP service that should be reachable from the matrix-tunnel server's loopback. RemotePort=0 lets the server auto-assign; we usually pin it.

type Tunnel

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

Tunnel wraps a matrix-tunnel client. Call Run, then Close.

The embedded FRP Service is one-shot — once its Run returns, its internal goroutines are torn down and it cannot be restarted. The library has its own reconnect loop, but we've observed it give up silently on yamux "session shutdown" (FRP client/control.go:130), leaving the outpost process alive with no tunnel. Tunnel.Run wraps svc.Run in a supervisor that rebuilds the service whenever it exits before ctx is canceled.

func NewTunnel

func NewTunnel(tc TunnelConfig, proxies []TCPProxy, visitors []STCPVisitor) (*Tunnel, error)

NewTunnel builds the matrix-tunnel client with the given proxies (and optional STCP visitors) pre-registered via the in-memory ConfigSource — no config-file path involved. Pass nil for visitors when only legacy outbound-proxy behavior is needed.

func (*Tunnel) Close

func (t *Tunnel) Close()

Close releases client resources.

func (*Tunnel) Run

func (t *Tunnel) Run(ctx context.Context) error

Run blocks until ctx is canceled. If the underlying FRP service exits early (e.g. yamux session shutdown that its own retry loop swallows), rebuild it and try again with exponential backoff. Only ctx cancellation terminates the loop.

type TunnelConfig

type TunnelConfig struct {
	ServerAddr string // required, e.g. "cloud.example.com"
	ServerPort int    // default 7000
	Token      string // shared secret with the cloudbox matrix-tunnel server
	User       string // optional proxy-name prefix; sets ClientCommonConfig.User

	// Protocol is the matrix-tunnel transport: "tcp" (default), "ws",
	// or "wss". When ws/wss is selected the agent dials cloudbox's HTTPS
	// port at the well-known path /~!frp (hardcoded by the underlying
	// tunnel library) and cloudbox's WS bridge pipes the upgraded conn
	// to the loopback tunnel server. Cloudflare/DO App Platform only
	// route HTTP(S) — wss is what makes the prod tunnel work.
	Protocol string
}

TunnelConfig is the minimal config for embedding the matrix-tunnel client (the underlying transport is fatedier/frp; we keep the import for the implementation but consistently refer to it as the matrix tunnel everywhere else).

type UpdateModeProvider

type UpdateModeProvider func() string

UpdateModeProvider returns the current host's update_mode for inclusion in the /apps envelope. Threaded as a closure so routes.go doesn't need to import conf or admincore — the live FileConfig value is re-read on each poll, picking up just-flipped settings.

Directories

Path Synopsis
Package admincore holds the protocol-agnostic configuration operations outpost exposes — pairing, app CRUD, outbound mounts, built-in toggles, cluster kubeconfig, restart.
Package admincore holds the protocol-agnostic configuration operations outpost exposes — pairing, app CRUD, outbound mounts, built-in toggles, cluster kubeconfig, restart.
Package adminui serves the local-only configuration web UI for outpost.
Package adminui serves the local-only configuration web UI for outpost.
Package apphealth provides per-app reachability measurement over outpost-owned TCP/HTTP paths (no ICMP/raw sockets).
Package apphealth provides per-app reachability measurement over outpost-owned TCP/HTTP paths (no ICMP/raw sockets).
Package backup is the outpost-side, app-opaque folder watcher that produces backup candidates on a cron schedule.
Package backup is the outpost-side, app-opaque folder watcher that produces backup candidates on a cron schedule.
Package brain is cloudbox/outpost/bashy's built-in decision-maker — the faculty that lets the platform DECIDE, not just execute, for any operation that needs judgment.
Package brain is cloudbox/outpost/bashy's built-in decision-maker — the faculty that lets the platform DECIDE, not just execute, for any operation that needs judgment.
Package clusterllm is outpost's passive integrator for an intra-home distributed-inference backend — a runtime that tensor/pipeline-splits a single model across several member machines so a home can serve a model too large for any one box.
Package clusterllm is outpost's passive integrator for an intra-home distributed-inference backend — a runtime that tensor/pipeline-splits a single model across several member machines so a home can serve a model too large for any one box.
Package conf holds the matrix-agent runtime configuration.
Package conf holds the matrix-agent runtime configuration.
mDNS advertisement: register ourselves on `_outpost._tcp.local`.
mDNS advertisement: register ourselves on `_outpost._tcp.local`.
Package fleetreg pushes this host's fleet inventory — the tools, agents, and skills it has installed — up to cloudbox.
Package fleetreg pushes this host's fleet inventory — the tools, agents, and skills it has installed — up to cloudbox.
Package heartbeat owns the outpost → cloudbox active liveness push (Layer-5 defense).
Package heartbeat owns the outpost → cloudbox active liveness push (Layer-5 defense).
Package hostauth verifies the host OS's own credentials.
Package hostauth verifies the host OS's own credentials.
Package mcpapi exposes outpost's configuration surface to agent tools (Claude Code, Windsurf, the outpost CLI, ...) over the Model Context Protocol.
Package mcpapi exposes outpost's configuration surface to agent tools (Claude Code, Windsurf, the outpost CLI, ...) over the Model Context Protocol.
Package mesh is the outpost's libp2p peer data plane — the node that carries authenticated, encrypted, NAT-traversing peer↔peer streams.
Package mesh is the outpost's libp2p peer data plane — the node that carries authenticated, encrypted, NAT-traversing peer↔peer streams.
Package mirror supervises mobility-aware continuous directory mirrors.
Package mirror supervises mobility-aware continuous directory mirrors.
Package ollama owns the outpost-side of the LLM pool: it watches the local Ollama daemon's model inventory, publishes the inventory to cloudbox so the pool scheduler can route by model presence, and tracks in-flight request counts so cloudbox can avoid over-scheduling a host with limited GPU capacity.
Package ollama owns the outpost-side of the LLM pool: it watches the local Ollama daemon's model inventory, publishes the inventory to cloudbox so the pool scheduler can route by model presence, and tracks in-flight request counts so cloudbox can avoid over-scheduling a host with limited GPU capacity.
Package osversion returns a one-line human-readable OS version label for the running host, e.g.
Package osversion returns a one-line human-readable OS version label for the running host, e.g.
Package otel discovers the local `ycode serve` observability stack (Prometheus + Alertmanager + VictoriaLogs + Jaeger + Perses, all reverse-proxied under one bearer-authed HTTP server) and lets outpost expose each surface through the matrix tunnel as a built-in app.
Package otel discovers the local `ycode serve` observability stack (Prometheus + Alertmanager + VictoriaLogs + Jaeger + Perses, all reverse-proxied under one bearer-authed HTTP server) and lets outpost expose each surface through the matrix tunnel as a built-in app.
Package peerhosts caches the list of paired outpost hostnames as returned by cloudbox's /api/v1/ssh/hosts endpoint.
Package peerhosts caches the list of paired outpost hostnames as returned by cloudbox's /api/v1/ssh/hosts endpoint.
Package peerplane is the outpost side of the dhnt p2p resource fabric's peer data plane.
Package peerplane is the outpost side of the dhnt p2p resource fabric's peer data plane.
Package peerstatus is a thin client for cloudbox's GET /api/v1/peers — the peer status board.
Package peerstatus is a thin client for cloudbox's GET /api/v1/peers — the peer status board.
Package peerticket verifies short-lived JWTs ("peer tickets") cloudbox issues at `POST /api/v1/ssh/peer-ticket`.
Package peerticket verifies short-lived JWTs ("peer tickets") cloudbox issues at `POST /api/v1/ssh/peer-ticket`.
Package portal speaks to the cloud portal's pairing endpoint (POST /api/register/exchange).
Package portal speaks to the cloud portal's pairing endpoint (POST /api/register/exchange).
Package repair provides the POST /admin/repair receiver — the cloudbox-driven trigger that starts a CI-failure self-fix on this host.
Package repair provides the POST /admin/repair receiver — the cloudbox-driven trigger that starts a CI-failure self-fix on this host.
Package runtime supervises a podman container that hosts this outpost's k3s-agent kubelet.
Package runtime supervises a podman container that hosts this outpost's k3s-agent kubelet.
image/cni command
Command outpost-cni implements a minimal Container Network Interface (CNI) plugin for Phase 3 of the outpost overlay design.
Command outpost-cni implements a minimal Container Network Interface (CNI) plugin for Phase 3 of the outpost overlay design.
image/cni/internal/plugin
Package plugin contains the load-bearing logic for the outpost-cni binary, factored out so the tiny main package stays under 100 lines.
Package plugin contains the load-bearing logic for the outpost-cni binary, factored out so the tiny main package stays under 100 lines.
Package sandbox implements outpost's safe-by-default container "sandbox" provider: a filtered libpod/docker API proxy that strips the escape-bearing knobs (privileged, host namespaces, host bind-mounts, added capabilities, devices) and injects per-request resource caps, so a remote caller who clears the cloudbox elevation gate can run containers without getting root-equivalent control of the host.
Package sandbox implements outpost's safe-by-default container "sandbox" provider: a filtered libpod/docker API proxy that strips the escape-bearing knobs (privileged, host namespaces, host bind-mounts, added capabilities, devices) and injects per-request resource caps, so a remote caller who clears the cloudbox elevation gate can run containers without getting root-equivalent control of the host.
Package selfcheck owns the Layer-2 defense: detect partial outpost corruption and self-heal from durable inputs.
Package selfcheck owns the Layer-2 defense: detect partial outpost corruption and self-heal from durable inputs.
Package shard orchestrates a Prima.cpp pipelined-ring shard over the mesh forwarder: serving a model bigger than any single node by splitting its layers across paired mesh peers.
Package shard orchestrates a Prima.cpp pipelined-ring shard over the mesh forwarder: serving a model bigger than any single node by splitting its layers across paired mesh peers.
Embedded coreutils fallback for the in-process shell.
Embedded coreutils fallback for the in-process shell.
In-process SSH client over the cloudbox matrix tunnel.
In-process SSH client over the cloudbox matrix tunnel.
Package supervisor is a minimal process supervisor: it keeps a fixed set of child Programs alive — start them, restart on exit with capped backoff, and gracefully stop them on shutdown.
Package supervisor is a minimal process supervisor: it keeps a fixed set of child Programs alive — start them, restart on exit with capped backoff, and gracefully stop them on shutdown.
Package sysinfo collects host capability information the outpost reports to cloudbox via the /apps poll loop.
Package sysinfo collects host capability information the outpost reports to cloudbox via the /apps poll loop.
Package sysload is the outpost's considerate system-load profiler.
Package sysload is the outpost's considerate system-load profiler.
Package upgrade carries the self-upgrade machinery shared by the CLI (`outpost upgrade`, `outpost rollback`) and the cloudbox-pushed daemon route (POST /admin/upgrade).
Package upgrade carries the self-upgrade machinery shared by the CLI (`outpost upgrade`, `outpost rollback`) and the cloudbox-pushed daemon route (POST /admin/upgrade).
Package userkube owns the workflow for materializing a kubectl- ready kubeconfig from cloudbox onto this host's disk.
Package userkube owns the workflow for materializing a kubectl- ready kubeconfig from cloudbox onto this host's disk.
Package vknode is the per-outpost half of the cloudbox cluster: it joins a cloud-side Kubernetes API server as a virtual node and runs scheduled Pods as podman containers on the host.
Package vknode is the per-outpost half of the cloudbox cluster: it joins a cloud-side Kubernetes API server as a virtual node and runs scheduled Pods as podman containers on the host.
Package warm implements the outpost's adaptive, considerate, always-on warm-serving plane.
Package warm implements the outpost's adaptive, considerate, always-on warm-serving plane.
Package ycode discovers and lifecycle-manages a `ycode serve` process running side-by-side with outpost on the same OS user account.
Package ycode discovers and lifecycle-manages a `ycode serve` process running side-by-side with outpost on the same OS user account.

Jump to

Keyboard shortcuts

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