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
- func LoadOrCreateHostKey() (ssh.Signer, error)
- func RegisterProvisionRoutes(rg gin.IRouter, deps ProvisionDeps)
- func RegisterRoutes(rg *gin.RouterGroup, deps Deps)
- 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) 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) 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 TCPProxy
- type Tunnel
- type TunnelConfig
- type UpdateModeProvider
Constants ¶
const ( BuiltinPodman = "podman" BuiltinOllama = "ollama" )
Builtin names — also the proxy slot names the admin UI surfaces and that get registered into the AppRegistry when enabled.
Variables ¶
This section is empty.
Functions ¶
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.
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"`
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
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
}
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) 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) 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"
Arch string `json:"arch,omitempty"` // runtime.GOARCH — "arm64" / "amd64"
}
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.
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
// 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)
// 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
}
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 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) (*Tunnel, error)
NewTunnel builds the matrix-tunnel client with the given proxies pre-registered via the in-memory ConfigSource — no config-file path involved.
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 conf holds the matrix-agent runtime configuration.
|
Package conf holds the matrix-agent runtime configuration. |
|
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 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 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 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 shell is the in-process bash interpreter (qiangli/sh / mvdan.cc/sh) wrapped in a PTY so xterm.js sees a real TTY: line discipline, echo, backspace, resize, and Ctrl-C all flow through the kernel TTY layer just as they would for a child `bash` process — except there is no child process.
|
Package shell is the in-process bash interpreter (qiangli/sh / mvdan.cc/sh) wrapped in a PTY so xterm.js sees a real TTY: line discipline, echo, backspace, resize, and Ctrl-C all flow through the kernel TTY layer just as they would for a child `bash` process — except there is no child process. |
|
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 vkpodman 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 vkpodman 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. |