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
- Variables
- func InitBootCount(dir string)
- func LoadOrCreateHostKey() (ssh.Signer, error)
- func RegisterProvisionRoutes(rg gin.IRouter, deps ProvisionDeps)
- func RegisterRoutes(rg *gin.RouterGroup, deps Deps)
- func ServeLANListener(ctx context.Context, ln net.Listener, deps sshHandlerDeps) error
- func ServeLANSSH(ctx context.Context, ln net.Listener, deps Deps) error
- func ServeLANSSHWS(ctx context.Context, ln net.Listener, deps Deps) error
- func StartEchoServer(addr string) (net.Listener, error)
- type AdminSet
- type AppCapabilities
- type AppEntry
- type AppMeta
- type AppRegistry
- func (r *AppRegistry) AddIntercept(name, prefix string, h http.Handler)
- func (r *AppRegistry) Entries() []AppEntry
- func (r *AppRegistry) LookupByProvisioningToken(token string) (string, bool)
- func (r *AppRegistry) LookupTCP(name string) string
- func (r *AppRegistry) LookupTarget(name string) *url.URL
- func (r *AppRegistry) Names() []string
- func (r *AppRegistry) ProvisioningToken(name string) string
- func (r *AppRegistry) ProxyTo(c *gin.Context, name, rest string)
- func (r *AppRegistry) Register(name, target string) error
- func (r *AppRegistry) RegisterFromConfig(ac conf.AppConfig) error
- func (r *AppRegistry) RegisterWithMeta(name, target string, meta AppMeta) error
- func (r *AppRegistry) SSOSecret(name string) string
- func (r *AppRegistry) SetCapabilities(name string, caps *AppCapabilities)
- func (r *AppRegistry) SetProvisioningToken(name, token string)
- func (r *AppRegistry) SetProxyWrap(name string, wrap func(http.Handler) http.Handler)
- func (r *AppRegistry) SetSSOSecret(name, secret string)
- func (r *AppRegistry) Unregister(name string)
- type AuthRequest
- type AuthResponse
- type BuildInfo
- type BuiltinDetector
- type BuiltinTarget
- type Deps
- type OutboundManager
- func (m *OutboundManager) AutoReconnect()
- func (m *OutboundManager) Connect(path, password string) error
- func (m *OutboundManager) Disconnect(path string)
- func (m *OutboundManager) Has(path string) bool
- func (m *OutboundManager) List() []OutboundView
- func (m *OutboundManager) ProxyTo(c *gin.Context, path, rest string)
- func (m *OutboundManager) Register(cfgs []conf.OutboundConfig)
- func (m *OutboundManager) Stop()
- type OutboundView
- type ProvisionDeps
- type STCPVisitor
- type TCPProxy
- type Tunnel
- type TunnelConfig
- type UpdateModeProvider
Constants ¶
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 ¶
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 ¶
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
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
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
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.
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 ¶
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".
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 ¶
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 ¶
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
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 ¶
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.
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.
Source Files
¶
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. |