extensions

package
v0.703.4 Latest Latest
Warning

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

Go to latest
Published: Sep 7, 2026 License: MIT Imports: 24 Imported by: 0

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

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

View Source
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

func ApplyTools(mgr *extension.Manager, coreTools []tools.BaseTool) []tools.BaseTool

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

func Commands() []*cobra.Command

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 ForwardConfigEvents added in v0.703.4

func ForwardConfigEvents(ctx context.Context, mgr *extension.Manager)

ForwardConfigEvents delivers configuration changes to every EventSubscriber that asked for extension.TopicConfig, until ctx is cancelled. It returns immediately; the fan-out runs in its own goroutine, and nothing at all is started when no extension subscribes.

func Frontend

func Frontend(mgr *extension.Manager, core fs.FS) fs.FS

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

func HasEventSubscribers(mgr *extension.Manager) bool

HasEventSubscribers reports whether anything would consume forwarded events. Callers use it to avoid starting fan-out goroutines in a standard build.

func HasFrontendExtensions

func HasFrontendExtensions(mgr *extension.Manager) bool

HasFrontendExtensions reports whether anything would change the asset tree, so callers can skip the composition entirely on a standard build.

func HasMemoryExtensions

func HasMemoryExtensions(mgr *extension.Manager) bool

HasMemoryExtensions reports whether anything in mgr uses the memory capability, as either a sink or a search wrapper.

func HasMemorySinks

func HasMemorySinks(mgr *extension.Manager) bool

HasMemorySinks reports whether any loaded extension observes memory writes.

func IdentityResolver added in v0.703.4

func IdentityResolver(mgr *extension.Manager) func(ctx context.Context) (Identity, bool)

IdentityResolver returns a function that reports the current identity, or false when nothing knows one. The returned function is safe for concurrent use and always non-nil, so callers need no nil check; with no provider loaded it simply always returns false and the host behaves exactly as an unextended Pando.

func InvalidateUIPolicy added in v0.703.4

func InvalidateUIPolicy()

InvalidateUIPolicy drops the memoised policy so the next read asks the providers again. Call it when something is known to have changed the policy and the surface cannot wait for the memo to expire.

func Load

func Load(ctx context.Context, opts Options) *extension.Manager

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

func NewManager(opts Options) *extension.Manager

NewManager builds the extension manager from the current configuration without loading anything yet.

func ProviderRequestDecorator added in v0.703.4

func ProviderRequestDecorator(mgr *extension.Manager) provider.RequestDecorator

ProviderRequestDecorator returns the per-request header hook for the loaded extensions, or nil when nothing implements the capability. Passing nil to provider.SetRequestDecorator is the same as never having called it, so the caller can wire the result unconditionally.

func RegisterConfigOverlays added in v0.703.4

func RegisterConfigOverlays(ctx context.Context, mgr *extension.Manager) error

RegisterConfigOverlays registers every loaded extension that provides a configuration overlay and applies the result.

Overlays are collected in extension load order, so an extension that depends on another (RequiresExtensions) overrides it, which is the same precedence the rest of the extension system uses.

Applying reloads the configuration. Subsystems that read config.Get() see the overlaid values immediately; subsystems that copied a value out at construction time learn about the change from the config event bus, where an overlay publishes an "overlay_applied" event naming the keys it changed.

func RegisterRoutes

func RegisterRoutes(mgr *extension.Manager, mux *http.ServeMux) []string

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 RegisterUIPolicy added in v0.703.4

func RegisterUIPolicy(mgr *extension.Manager)

RegisterUIPolicy installs the merged policy of mgr as the process-wide one and points the configuration write path at it, so a hidden or read-only path is refused as well as not offered.

It is safe to call with a manager that has no provider: that installs the empty policy, which is what an unextended Pando has.

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.

func SetPromptRunner added in v0.703.4

func SetPromptRunner(fn PromptRunFunc)

SetPromptRunner registers the host's prompt runner. Passing nil unregisters it, which is what tests and teardown do.

func SetUIPolicyResolver added in v0.703.4

func SetUIPolicyResolver(fn func(ctx context.Context) UIPolicy)

SetUIPolicyResolver replaces the process-wide resolver. Passing nil removes it, restoring the unextended behaviour; that is what a host tearing a manager down does, and what a test does when it is finished.

func StartEventPublisher added in v0.703.4

func StartEventPublisher(ctx context.Context, mgr *extension.Manager)

StartEventPublisher routes events published through internal/extevents to the loaded subscribers until ctx is cancelled. Nothing is installed when no extension subscribes, which leaves every publish site in core at one atomic load.

func UIPolicyResolver added in v0.703.4

func UIPolicyResolver(mgr *extension.Manager) func(ctx context.Context) UIPolicy

UIPolicyResolver returns a function reporting the merged policy of every loaded provider. The returned function is safe for concurrent use and always non-nil, so callers need no nil check; with no provider loaded it always returns the zero policy and every surface behaves exactly as an unextended Pando.

func WrapHTTP

func WrapHTTP(mgr *extension.Manager, next http.Handler) http.Handler

WrapHTTP applies every loaded HTTPMiddlewareProvider around next. The highest priority ends up outermost, so it sees the request first — which is what an authentication middleware needs.

Types

type Attribution

type Attribution struct {
	ProjectID  string
	UserID     string
	InstanceID string
}

Attribution identifies the instance a memory event came from. It is attribution only: isolation belongs to whatever store the sink talks to.

type Identity added in v0.703.4

type Identity struct {
	UserID   string
	Email    string
	DeviceID string
	Groups   []string
}

Identity is the host-side copy of extension.Identity. The duplication is the same one internal/config makes for overlays: core packages consume this type without importing the extension contract, and the contract stays free of internal types.

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

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.

func Panels

func Panels(mgr *extension.Manager) []Panel

Panels returns the merged UI manifest of every loaded FrontendProvider, sorted by slot, then declared order, then extension ID, so the shell renders the same layout on every start.

A nil manager returns nil, which the endpoint serves as an empty list.

type PromptRunFunc added in v0.703.4

type PromptRunFunc func(ctx context.Context, req extension.PromptRequest) (extension.PromptResult, error)

PromptRunFunc performs one non-interactive turn. internal/app implements it.

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.

type UIPolicy added in v0.703.4

type UIPolicy struct {
	// HiddenSections lists the configuration paths no surface should render.
	HiddenSections []string
	// ReadOnlySections lists the paths rendered with their value but not
	// editable.
	ReadOnlySections []string
	// ReadOnlyLabel is the caller-supplied text shown on a read-only field.
	// Empty means the surface uses its own generic marker.
	ReadOnlyLabel string
	// Banner is the notice shown above the settings.
	Banner UIPolicyBanner
}

UIPolicy is the host-side copy of extension.UIPolicy, the same duplication the overlay and identity capabilities make: core packages consume this type without importing the extension contract.

func CurrentUIPolicy added in v0.703.4

func CurrentUIPolicy(ctx context.Context) UIPolicy

CurrentUIPolicy returns the process-wide merged policy, memoised for uiPolicyTTL. With no resolver installed it returns the zero policy.

The providers are asked outside the lock, and a call that arrives while another is resolving gets the previous value rather than starting a second resolution. That is what makes the call re-entrant: a provider that derives its policy from the configuration reaches the lock check, which reads the restricted paths, which lands back here, and must find an answer instead of its own call.

func (UIPolicy) Empty added in v0.703.4

func (p UIPolicy) Empty() bool

Empty reports whether the policy asks for nothing, which is the state every standalone Pando is in.

func (UIPolicy) IsHidden added in v0.703.4

func (p UIPolicy) IsHidden(path string) bool

IsHidden reports whether path is covered by a hidden section. Coverage is the same both-directions, case-insensitive segment match the lock list uses, so a hidden "providerAccounts" covers "providerAccounts.anthropic.apiKey".

func (UIPolicy) IsReadOnly added in v0.703.4

func (p UIPolicy) IsReadOnly(path string) bool

IsReadOnly reports whether path is covered by a read-only section. A hidden path is not reported as read-only: it is not rendered at all, and the stronger statement wins.

func (UIPolicy) RestrictedPaths added in v0.703.4

func (p UIPolicy) RestrictedPaths() []string

RestrictedPaths returns every path the policy withdraws from local control, hidden and read-only together, sorted and deduplicated. It is what the configuration write path refuses.

type UIPolicyBanner added in v0.703.4

type UIPolicyBanner struct {
	Text string
	Link string
}

UIPolicyBanner is the host-side copy of extension.UIPolicyBanner.

Jump to

Keyboard shortcuts

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