agent

package
v0.0.0-test Latest Latest
Warning

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

Go to latest
Published: May 26, 2026 License: MIT Imports: 44 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"
)

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

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 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"`
	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

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) 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

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.

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

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) (*Tunnel, error)

NewTunnel builds the matrix-tunnel client with the given proxies pre-registered via the in-memory ConfigSource — no config-file path involved.

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 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.

Jump to

Keyboard shortcuts

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