Documentation
¶
Overview ¶
Package extensions is the host side of the extension system: it adapts Pando's internal configuration and services to the public contract declared in pkg/extension, and builds the process-wide extension manager.
The split matters: pkg/extension may not import internal packages, because out-of-tree modules (the private enterprise module) import it. Everything that needs to touch internal/ lives here instead.
Index ¶
- Constants
- func ApplyTools(mgr *extension.Manager, coreTools []tools.BaseTool) []tools.BaseTool
- func Commands() []*cobra.Command
- func Forward[T any](ctx context.Context, mgr *extension.Manager, topic string, ...)
- func Frontend(mgr *extension.Manager, core fs.FS) fs.FS
- func HasEventSubscribers(mgr *extension.Manager) bool
- func HasFrontendExtensions(mgr *extension.Manager) bool
- func HasMemoryExtensions(mgr *extension.Manager) bool
- func HasMemorySinks(mgr *extension.Manager) bool
- func Load(ctx context.Context, opts Options) *extension.Manager
- func NewManager(opts Options) *extension.Manager
- func RegisterRoutes(mgr *extension.Manager, mux *http.ServeMux) []string
- func SearchMiddleware(mgr *extension.Manager) kb.SearchMiddleware
- func WrapHTTP(mgr *extension.Manager, next http.Handler) http.Handler
- type Attribution
- type MemoryPublisher
- type MemorySinkStatus
- type MemoryStatus
- type Options
- type Panel
- type PublisherStats
Constants ¶
const AssetPrefix = "ext"
AssetPrefix is where extension frontend assets live in the served asset tree. They are folded into the *static* layer rather than mounted on the API mux for one concrete reason: a browser cannot attach an Authorization header to a dynamic import(), so a panel bundle behind the API token check could never be loaded. Extension assets are therefore public, exactly like core's own JavaScript — see the warning on extension.FrontendProvider.
const ExtRoutePrefix = "/api/ext/"
ExtRoutePrefix is the single prefix every extension HTTP route lives under. Nothing an extension serves is reachable outside it, which is what makes the API surface auditable and lets a reverse proxy treat extensions as one block.
Variables ¶
This section is empty.
Functions ¶
func ApplyTools ¶
ApplyTools returns the tool set the model should see: the core tools passed in, plus every tool contributed by a loaded ToolProvider, with every ToolFilter applied and every ToolInterceptor wrapped around each tool.
A nil manager (no extensions in the build, or a code path that never loaded them) returns coreTools untouched, so callers do not need to guard.
func Commands ¶
Commands builds the cobra tree for every command contributed by an extension compiled into this binary. It is safe to call from init(): nothing is provisioned and no configuration is required.
Whether an extension is actually enabled is decided when a command runs, not here — see commandRunner.
Name collisions are rejected rather than resolved: two extensions claiming `sync` is a build-time mistake, and silently dropping one would make the binary's behaviour depend on registration order.
func Forward ¶
func Forward[T any](ctx context.Context, mgr *extension.Manager, topic string, src pubsub.Suscriber[T])
Forward subscribes to src and delivers its events to every EventSubscriber that asked for topic, until ctx is cancelled. It returns immediately; the fan-out runs in its own goroutine.
Events are dropped, never queued, when a subscriber is slow: the alternative is unbounded memory growth in the host because an extension misbehaves. The contract says so, and an extension that must not lose events buffers them.
func Frontend ¶
Frontend returns the asset tree to serve: core's own assets, with any extension replacement, overlay and asset subtree applied.
It is the single place the three frontend mechanisms meet, so their precedence is decided once and is readable in one function:
overlay files > base (replacement, else core) > extension subtrees
A nil manager returns core unchanged, so callers need no guard.
func HasEventSubscribers ¶
HasEventSubscribers reports whether anything would consume forwarded events. Callers use it to avoid starting fan-out goroutines in a standard build.
func HasFrontendExtensions ¶
HasFrontendExtensions reports whether anything would change the asset tree, so callers can skip the composition entirely on a standard build.
func HasMemoryExtensions ¶
HasMemoryExtensions reports whether anything in mgr uses the memory capability, as either a sink or a search wrapper.
func HasMemorySinks ¶
HasMemorySinks reports whether any loaded extension observes memory writes.
func Load ¶
Load builds the manager and loads every enabled extension.
A failure inside one extension is logged and recorded in its status, never returned as a startup error: an optional feature must not stop Pando from running. The manager is always usable, even when empty.
func NewManager ¶
NewManager builds the extension manager from the current configuration without loading anything yet.
func RegisterRoutes ¶
RegisterRoutes mounts the routes of every loaded HTTPEndpointProvider on mux. It returns the full patterns registered, for logging and tests.
A nil manager registers nothing, so callers need no guard.
func SearchMiddleware ¶
func SearchMiddleware(mgr *extension.Manager) kb.SearchMiddleware
SearchMiddleware builds the kb.SearchMiddleware for the wrappers in mgr, or nil when there are none. Chaining runs in registration order, so the first registered wrapper ends up outermost.
Types ¶
type Attribution ¶
Attribution identifies the instance a memory event came from. It is attribution only: isolation belongs to whatever store the sink talks to.
type MemoryPublisher ¶
type MemoryPublisher struct {
// contains filtered or unexported fields
}
MemoryPublisher fans committed remembrance writes out to the MemorySinks in a manager. The zero value is not usable; build one with NewMemoryPublisher.
func NewMemoryPublisher ¶
func NewMemoryPublisher(mgr *extension.Manager, cfg config.ExtensionsMemoryConfig, attr func() Attribution) *MemoryPublisher
NewMemoryPublisher builds a publisher for the sinks in mgr. It returns nil when the capability is switched off, when nothing implements MemorySink, or when the configuration enables the capability without naming a single scope — an empty scope list shares nothing, and saying so out loud beats silently publishing everything or silently publishing nothing. attr is called per event rather than captured once, because the instance ID is only assigned after the IPC lock is taken, which happens later in startup than this wiring does.
func (*MemoryPublisher) Close ¶
func (p *MemoryPublisher) Close()
Close stops the async worker and waits for the queue to drain. Safe to call on a nil publisher and safe to call twice.
func (*MemoryPublisher) Observer ¶
func (p *MemoryPublisher) Observer() kb.WriteObserver
Observer returns the kb.WriteObserver to install on the store.
func (*MemoryPublisher) Stats ¶
func (p *MemoryPublisher) Stats() PublisherStats
Stats returns the host counters. Safe on a nil publisher.
type MemorySinkStatus ¶
type MemorySinkStatus struct {
ID string `json:"id"`
Name string `json:"name"`
Active bool `json:"active"`
DryRun bool `json:"dryRun"`
Destination string `json:"destination,omitempty"`
Scopes []string `json:"scopes,omitempty"`
Pending int `json:"pending"`
Sent int64 `json:"sent"`
Dropped int64 `json:"dropped"`
LastSyncAt string `json:"lastSyncAt,omitempty"`
LastError string `json:"lastError,omitempty"`
// Reports is false when the sink does not implement MemorySyncReporter, so
// the UI can say "shipping, state unknown" instead of "idle" — which would
// be a lie in exactly the case that matters.
Reports bool `json:"reports"`
}
MemorySinkStatus is one sink's reported state.
type MemoryStatus ¶
type MemoryStatus struct {
// Enabled reflects the configuration gate, not whether a sink exists.
Enabled bool `json:"enabled"`
// Active is true when the gate is open and something is actually wired to
// receive events.
Active bool `json:"active"`
DryRun bool `json:"dryRun"`
Mode string `json:"mode"`
Scopes []string `json:"scopes,omitempty"`
Paths []string `json:"paths,omitempty"`
Origins []string `json:"origins,omitempty"`
WrapSearch bool `json:"wrapSearch"`
Wrappers []string `json:"wrappers,omitempty"`
Host PublisherStats `json:"host"`
Sinks []MemorySinkStatus `json:"sinks"`
}
MemoryStatus is what /api/v1/extensions/memory returns.
func MemoryStatusOf ¶
func MemoryStatusOf(mgr *extension.Manager, cfg config.ExtensionsMemoryConfig, pub *MemoryPublisher) MemoryStatus
MemoryStatusOf assembles the status. Every field tolerates a nil manager or publisher: a standard build answers "off" rather than 404, so the UI has one code path instead of two.
type Options ¶
type Options struct {
// Config is the configuration to read from. Defaults to config.Get().
Config *config.Config
// Logger receives extension lifecycle messages. Defaults to the Pando
// structured logger.
Logger *slog.Logger
}
Options overrides how the manager is built. The zero value is what production code uses.
type Panel ¶
type Panel struct {
ID string `json:"id"`
Extension string `json:"extension"`
Title string `json:"title"`
Slot string `json:"slot"`
Entry string `json:"entry"`
Icon string `json:"icon,omitempty"`
Order int `json:"order"`
}
Panel is one entry of the merged UI manifest served to the shell. It is the wire shape: the extension declares a relative Entry, core resolves it to a URL the browser can import.
type PublisherStats ¶
type PublisherStats struct {
Sinks int `json:"sinks"`
Published int64 `json:"published"`
Filtered int64 `json:"filtered"`
Dropped int64 `json:"dropped"`
Failed int64 `json:"failed"`
Queued int `json:"queued"`
}
PublisherStats are the host's own counters, independent of what each sink reports about itself.