extension

package
v0.704.1 Latest Latest
Warning

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

Go to latest
Published: Sep 8, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package extension is the public, stable contract for Pando extensions.

An extension is a Go package compiled into the Pando binary that adds capabilities (tools, HTTP endpoints, frontend assets, memory sinks, ...) without the core knowing about it. Registration happens at init() time:

func init() { extension.Register(MyExtension{}) }

Importing the package is the installation. A build that should contain an extension blank-imports it; a build that should not, does not. This is the Caddy model, and the same one already used by remembrances-mcp.

Rules for this package

This package is imported by out-of-tree modules (notably the private enterprise module github.com/digiogithub/alchemai-agent), which means:

  1. It must never import github.com/digiogithub/pando/internal/... — Go forbids that from another module, so any such import would silently make the contract unusable outside this repository.
  2. Every type crossing the boundary is declared here, in terms of the standard library only. Core adapts these types to its internal ones.
  3. Changes here are contract changes. Adding a capability interface or an optional method is safe (capabilities are discovered by type assertion); changing an existing signature is not.

Lifecycle

New() -> Provision(ctx, HostServices) -> Validate() -> [use] -> Cleanup()

Provision and Validate and Cleanup are all optional: implement only the ones the extension needs, and the manager discovers them by type assertion.

Index

Constants

View Source
const (
	TopicSession    = "session"
	TopicMessage    = "message"
	TopicPermission = "permission"

	// TopicConfig carries host configuration changes. Unlike the resource
	// topics its events are not about one identified object: ID and SessionID
	// are empty, Type says what happened (EventUpdated for an ordinary change,
	// EventOverlayApplied, EventConfigReloaded) and the payload carries
	//
	//	"event"       string   the host's own name for the change, if any
	//	"section"     string   which part of the configuration moved, or ""
	//	"source"      string   where the change came from ("file", "tui",
	//	                       "webui", "overlay", "reload")
	//	"changedKeys" []any    dotted paths whose value changed, when the
	//	                       publisher can name them; absent means unknown,
	//	                       so assume the whole section moved
	//	"lockedKeys"  []any    the lock list as it stands after the change
	//	"timestamp"   string   RFC 3339
	//
	// This is how an extension that imposes configuration learns that its
	// values went live, and how it notices a local edit fighting its overlay.
	TopicConfig = "config"

	// TopicTool carries one tool execution. Two events are published per call,
	// EventStarted when the host hands the call to the tool and EventCompleted
	// when the tool returns, both with the tool call id as ID. Payload:
	//
	//	"name"       string   the tool name the model asked for
	//	"agent"      string   the agent that is running the call
	//	"durationMs" float64  wall time of the call (EventCompleted only)
	//	"ok"         bool     whether the call succeeded (EventCompleted only)
	//	"error"      string   failure reason, absent when ok is true
	//
	// Arguments and results are deliberately absent: they are the user's
	// content, and a topic meant for accounting must not become a copy of the
	// conversation. An extension that needs them uses the tool capability,
	// where interception is explicit and visible to the user.
	TopicTool = "tool"

	// TopicMCP carries the outcome of an MCP server handshake, with the server
	// name as ID. Type is EventConnected, EventAuthRequired when the server
	// refused the host for want of credentials, or EventFailed. Payload:
	//
	//	"server"    string  the configured server name
	//	"transport" string  "stdio", "sse" or "http"
	//	"client"    string  which part of the host connected
	//	"error"     string  failure reason, absent on EventConnected
	//
	// No credential material is ever put in the payload.
	TopicMCP = "mcp"

	// TopicSkill reports that a skill was activated for a run, with the skill
	// name as ID and EventActivated as Type. Payload:
	//
	//	"name"   string  the skill name
	//	"agent"  string  the agent the skill was activated for
	//	"source" string  what activated it ("auto" for prompt matching)
	//
	// Skill instructions are not carried: the name is what an inventory or a
	// usage report needs, and the body may be private to the user.
	TopicSkill = "skill"

	// TopicProvider reports a change to the configured provider accounts, with
	// the account id as ID and EventCreated, EventUpdated or EventDeleted as
	// Type. Payload:
	//
	//	"id"           string  the account id
	//	"providerType" string  the provider the account belongs to
	//	"disabled"     bool    whether the account is currently disabled
	//
	// API keys, tokens and base URLs are never published: an observer is told
	// that the set of accounts moved, not what is in them.
	TopicProvider = "provider"
)

The topics core publishes. More may be added; an unknown topic is not an error, and a subscriber must ignore what it does not recognise.

View Source
const (
	// SlotSidebar adds a top-level navigation entry with its own page.
	SlotSidebar = "sidebar"
	// SlotSettings adds a section to the settings screen.
	SlotSettings = "settings"
	// SlotChatSide adds a panel to the chat information sidebar.
	SlotChatSide = "chat-side"
	// SlotStatusBar adds a small indicator to the status bar.
	SlotStatusBar = "status-bar"
)

UI slots a panel can mount into. The set is closed on purpose: every slot is a place the shell actually reserves, and an unknown slot is dropped rather than guessed at.

View Source
const DefaultConfigEnvPrefix = "PANDO"

DefaultConfigEnvPrefix is the environment-variable prefix HostServices uses when the host does not set one. It matches the prefix the configuration system itself binds, so the two halves of the configuration read from the same namespace.

Variables

View Source
var (
	ErrLicenseMalformed  = errors.New("extension: malformed license")
	ErrLicenseUnknownKey = errors.New("extension: license signed by an unknown key")
	ErrLicenseSignature  = errors.New("extension: license signature does not verify")
	ErrLicenseExpired    = errors.New("extension: license expired")
)

Errors returned by VerifyLicense. They are distinguished because they mean different things to whoever has to act on them: a bad signature is a broken or forged file, an unknown key is usually a build/key-rotation mismatch, and an expired license is a commercial matter, not a technical one.

Functions

func Capability

func Capability[T any](m *Manager) []T

Capability returns every loaded extension implementing T, in load order. It is how core subsystems find what extends them:

for _, p := range extension.Capability[extension.ToolProvider](mgr) { ... }

func Len

func Len() int

Len reports how many extensions are registered.

func Preview

func Preview[T any](m *Manager) []T

Preview instantiates every *registered* extension and returns the instances implementing T, without provisioning any of them.

It exists for surfaces that must be described before Pando is running: the CLI builds its command tree in init(), where no configuration has been read yet and no database may be opened. Preview therefore ignores configuration deliberately — help output describes what the binary contains, and whether an extension is enabled is checked when a command actually runs.

The instances returned are throwaway and unprovisioned: read declarations from them, never act. To act, Load the manager and take the provisioned instance from Instance(id).

func Register

func Register(e Extension)

Register adds an extension to the default registry. It is meant to be called from init() and panics on programmer error — an invalid ID, a missing factory, or a duplicate registration — because there is no sensible way to recover from a malformed extension at that point, and failing loudly at startup beats a silently missing feature.

func SignLicense

func SignLicense(claims LicenseClaims, keyID string, priv ed25519.PrivateKey) ([]byte, error)

SignLicense produces a signed license document. It lives here so that the issuing tool and the verifier can never drift apart on the format; the private key is the issuer's business and never appears in a build.

Types

type CleanerUpper

type CleanerUpper interface {
	Cleanup() error
}

CleanerUpper releases resources when the extension is unloaded or the process shuts down.

type Command

type Command struct {
	// Use is the command name, optionally followed by its argument sketch
	// ("sync [target]"). The first word must be unique among all extension
	// commands in the build.
	Use string
	// Short is the one-line description shown in the command list.
	Short string
	// Long is the full help text.
	Long string
	// Aliases are alternative names for the command.
	Aliases []string
	// Flags declares the command's flags.
	Flags []Flag
	// Run executes the command. args holds the positional arguments after the
	// command name. Returning an error makes the process exit non-zero with
	// that message.
	Run func(ctx context.Context, args []string, flags Flags) error
	// Subcommands nest below this one. Their Use names must be unique among
	// their siblings.
	Subcommands []Command
}

Command is one CLI subcommand of the pando binary.

Commands are mounted under the `ext` subcommand, never at the top level: `pando ext <use>`. That is deliberate — an extension cannot shadow a core command, and `pando ext --help` lists exactly what the build added.

type CommandProvider

type CommandProvider interface {
	Extension
	Commands() []Command
}

CommandProvider is implemented by extensions that add CLI subcommands.

Commands are collected before the manager provisions anything, because the CLI must be able to print help without starting Pando. An extension whose commands depend on provisioned state should do that work inside Run, not while building the Command value.

type ConfigOverlay added in v0.703.4

type ConfigOverlay struct {
	// Values is the overlay document. Its shape mirrors the configuration
	// file: section names at the top level, nested maps below. Keys absent
	// from it are left exactly as the host loaded them.
	//
	// Merge semantics, applied per key:
	//   - a scalar replaces the loaded value;
	//   - a map is merged key by key, recursively;
	//   - a list of objects that all carry an "id" is merged by that id, the
	//     overlay entries first, so an overlay can add or redefine one entry
	//     without restating the rest;
	//   - any other list replaces the loaded list, unless its path is named in
	//     Additive, in which case the two lists are unioned, loaded values
	//     first, duplicates dropped.
	Values map[string]any

	// Locked lists the paths the host must refuse to write locally. A write
	// through any configuration mutator that would change a locked path fails
	// with a typed error, and the surfaces report the key as managed by an
	// extension rather than as a failed save.
	//
	// Locking a path that the overlay does not set is allowed and means
	// "freeze whatever is there": the value stays whatever the files said, but
	// nobody may change it from inside Pando.
	Locked []string

	// Additive lists the list-valued paths that should be unioned with the
	// loaded value rather than replacing it. Use it for lists where local
	// entries are legitimate additions (extra context paths, extra banned
	// commands) rather than a competing choice.
	Additive []string

	// Source is a short human-readable label for where the document came from,
	// used in log lines and in the change event. Optional; the extension ID is
	// used when it is empty.
	Source string
}

ConfigOverlay is one configuration document an extension imposes, together with the keys it does not want edited locally.

Paths in Locked and Additive are dotted paths into the configuration document ("tui.theme", "internalTools.braveApiKey"), matched case-insensitively segment by segment, because configuration keys are case-insensitive throughout. A path names either a leaf or a whole subtree: locking "mcpServers" locks every server under it.

type ConfigOverlayController added in v0.703.4

type ConfigOverlayController interface {
	// ReapplyOverlays re-runs configuration load, asking every registered
	// provider for its current document. It returns the load error, if any.
	// Calls are serialised by the host; a call made while another is in
	// progress waits for it.
	ReapplyOverlays(ctx context.Context) error

	// RequestReload is the same operation, coalesced. Requests that arrive
	// close together share one reload and all return its outcome, so an
	// extension driven by a stream of remote notifications cannot make the
	// host reload once per notification. Prefer it for anything triggered by
	// an external event; use ReapplyOverlays when the caller needs the reload
	// to have finished by the time the call returns, with no delay.
	//
	// reason is a short label for the host log ("new policy generation"), not
	// something the host interprets.
	//
	// The error returned is the reload's own, so an extension learns that the
	// document it published cannot be loaded. A failed reload leaves the
	// previous configuration in effect. Cancelling ctx abandons the wait, not
	// the reload.
	//
	// Never call it from inside ConfigOverlay: the provider would be waiting
	// for a load that is waiting for the provider.
	RequestReload(ctx context.Context, reason string) error
}

ConfigOverlayController is the host side of the same capability: the handle an extension uses to tell the host that its overlay has changed.

It is deliberately not a "set this value" call. The extension says only that the answer to ConfigOverlay is now different; the host decides when to ask and re-runs the whole load path so that files, environment and every registered overlay are combined exactly as they are at startup.

Available as HostServices.ConfigOverlays. It is nil in hosts that do not support overlays, so check before calling.

type ConfigOverlayProvider added in v0.703.4

type ConfigOverlayProvider interface {
	Extension
	ConfigOverlay(ctx context.Context) (ConfigOverlay, error)
}

ConfigOverlayProvider is implemented by extensions that impose configuration on the host.

The host calls ConfigOverlay during configuration load, which happens before the extension is started and may happen again at any time afterwards. The call must therefore be cheap and must not block on the network: an extension that fetches its document remotely caches it and serves the cache here, refreshing out of band and calling ConfigOverlayController.ReapplyOverlays when the cache changes.

Returning an error means "I have nothing to say right now": the host logs it and continues with the configuration it already has. It is never a startup failure, because an optional capability must not be able to stop Pando from running.

type ConfigView

type ConfigView interface {
	// WorkingDir is the project root.
	WorkingDir() string
	// DataDir is the per-project Pando data directory (.pando/data).
	DataDir() string
	// Debug reports whether the host runs in debug mode.
	Debug() bool
	// Lookup resolves a dotted configuration path to a value, for the rare
	// case where an extension must read a core setting. Returns false when the
	// path is unknown. Implementations return copies, never live pointers.
	Lookup(path string) (any, bool)
	// LockedKeys lists the configuration paths currently locked by an overlay
	// (see ConfigOverlay.Locked), sorted and deduplicated. It is the state a
	// panel or a settings surface reads to render a key as managed rather than
	// editable. Empty when no overlay locks anything.
	LockedKeys() []string
}

ConfigView exposes the parts of the host configuration an extension may read. It is intentionally small: extensions configure themselves through their own subtree, and only consult the host for facts they cannot know.

type Entitlement

type Entitlement string

Entitlement names something a license permits. It is matched against extension IDs three ways:

memory.sink.corp   exact ID
memory.*           the namespace and everything under it
*                  every extension

func (Entitlement) Matches

func (e Entitlement) Matches(id ID) bool

Matches reports whether this entitlement covers the given extension ID.

type Entitlements

type Entitlements []Entitlement

Entitlements is the list carried by a license.

func (Entitlements) Allows

func (es Entitlements) Allows(id ID) bool

Allows reports whether any entitlement in the list covers the ID.

func (Entitlements) Strings

func (es Entitlements) Strings() []string

Strings renders the entitlements for reporting.

type Entry

type Entry struct {
	// Enabled switches the extension on. An extension is also loaded when it
	// has a non-empty Config, so that configuring one is enough to enable it.
	Enabled bool
	// Config is the extension's own configuration subtree.
	Config map[string]any
}

Entry is the per-extension configuration the host passes to the manager. It mirrors [Extensions.Entries."<id>"] in pando.toml.

type Event

type Event struct {
	// Topic identifies the resource kind (see the Topic constants).
	Topic string
	// Type is what happened.
	Type EventType
	// ID is the resource identifier when the payload carries one.
	ID string
	// SessionID is the session the resource belongs to, when it belongs to one.
	SessionID string
	// Payload is the resource as JSON-decoded values: strings, float64 numbers,
	// bools, maps and slices. One map is shared by every subscriber of the
	// event, so treat it as read-only and copy anything you keep past the call.
	Payload map[string]any
	// Time is when the host observed the event.
	Time time.Time
}

Event is one resource lifecycle notification.

type EventSubscriber

type EventSubscriber interface {
	Extension
	// Topics lists the topics to receive. An empty result means every topic,
	// including topics added in later versions.
	Topics() []string
	// HandleEvent is called from the host's fan-out goroutine and must return
	// promptly: slow work belongs on a queue the extension owns. Events are
	// dropped rather than queued when a subscriber cannot keep up, so an
	// extension that must not lose events has to buffer them itself.
	HandleEvent(ctx context.Context, ev Event)
}

EventSubscriber is implemented by extensions that observe resource lifecycle events. This is how a corporate memory sink learns that a session ended or a message was written without core knowing anything about it.

type EventType

type EventType string

EventType is what happened to a resource.

const (
	EventCreated EventType = "created"
	EventUpdated EventType = "updated"
	EventDeleted EventType = "deleted"

	// EventOverlayApplied is the Type of a TopicConfig event published after a
	// load in which a configuration overlay was merged. Payload["changedKeys"]
	// names the paths whose value the overlay moved.
	EventOverlayApplied EventType = "overlay_applied"

	// EventConfigReloaded is the Type of a TopicConfig event published after
	// the host reloaded its configuration, whatever caused it: a file change,
	// a settings save, an extension asking for one.
	EventConfigReloaded EventType = "config_reloaded"

	// EventStarted and EventCompleted bracket one activity that takes time.
	// They are always published as a pair on the same topic and with the same
	// ID, so a subscriber can measure or correlate them; EventCompleted also
	// carries the duration, so a subscriber that only wants the outcome may
	// ignore EventStarted entirely.
	EventStarted   EventType = "started"
	EventCompleted EventType = "completed"

	// EventConnected and EventFailed report the outcome of establishing a
	// connection to an external service.
	EventConnected EventType = "connected"
	EventFailed    EventType = "failed"

	// EventAuthRequired is the Type of an event reporting that an external
	// service refused the host for want of credentials, so a human has to
	// authorise it before it can be used.
	EventAuthRequired EventType = "auth_required"

	// EventActivated is the Type of an event reporting that an optional piece
	// of behaviour was switched on for a run.
	EventActivated EventType = "activated"
)

type Extension

type Extension interface {
	ExtensionInfo() Info
}

Extension is the base interface. Everything an extension can do beyond announcing itself comes from the optional interfaces below and from the capability interfaces in the rest of this package.

type Flag

type Flag struct {
	// Name is the long flag name, without dashes.
	Name string
	// Shorthand is the optional one-letter form, without the dash.
	Shorthand string
	// Usage is the one-line help text.
	Usage string
	// Value is the default value and, by its dynamic type, the flag type.
	Value any
}

Flag declares one command-line flag. Value carries both the default and the type: bool, string, int and []string are supported, and anything else is rejected when the command is registered.

type Flags

type Flags map[string]any

Flags is the parsed flag set handed to a command at run time. Missing keys mean the flag was never declared; a declared flag is always present, holding its default when the user did not pass it.

func (Flags) Bool

func (f Flags) Bool(name string) bool

Bool reads a boolean flag.

func (Flags) Int

func (f Flags) Int(name string) int

Int reads an integer flag. Both int and int64 are accepted so the same accessor works whichever numeric type the host's flag library produced.

func (Flags) String

func (f Flags) String(name string) string

String reads a string flag.

func (Flags) StringSlice

func (f Flags) StringSlice(name string) []string

StringSlice reads a repeated string flag.

type FrontendOverlay

type FrontendOverlay interface {
	Extension
	// OverlayAssets holds the replacement files, addressed by the same paths
	// they have in the core asset tree ("assets/logo.svg").
	OverlayAssets() fs.FS
	// Overrides lists the core asset paths this extension shadows, without a
	// leading slash. Files present in OverlayAssets but absent here are ignored.
	Overrides() []string
}

FrontendOverlay is implemented by extensions that replace individual core assets — branding, mostly: logo, theme stylesheet, favicon.

Overrides must be listed explicitly. A blanket shadow of the core asset tree would make every upgrade undebuggable, because a stale extension file would silently win over a new core one with no way to see it happening.

type FrontendProvider

type FrontendProvider interface {
	Extension
	// AssetPath is the single path segment this extension owns under /ext/.
	// Same rules as HTTPEndpointProvider.BasePath: lowercase letters, digits,
	// '_' or '-', unique in the build.
	AssetPath() string
	// Assets is the built frontend to serve, typically an //go:embed of the
	// extension's own dist directory. Returning nil contributes no files, which
	// is valid for an extension whose panels live in a shared bundle.
	Assets() fs.FS
	// Panels returns the panels to register. It may return nil: an extension
	// may want to serve assets without declaring a panel.
	Panels() []PanelManifest
}

FrontendProvider is implemented by extensions that add UI to the core WebUI.

Assets are served read-only under /ext/<AssetPath>/, alongside core's own static files and therefore *outside* the API token check — exactly like core's JavaScript, and necessarily so: a browser cannot attach headers to a dynamic import(). Never serve anything from Assets that is not safe to hand to an unauthenticated client; private data belongs behind an HTTPEndpointProvider route, which the panel then calls with the token.

type FrontendReplacer

type FrontendReplacer interface {
	Extension
	// ReplaceFrontend returns the asset root to serve instead of core's.
	// Returning nil declines, which lets configuration decide at run time.
	ReplaceFrontend() fs.FS
}

FrontendReplacer is implemented by an extension that ships an entire alternative frontend, replacing the core WebUI wholesale.

This is the mechanism for a differently-branded product built on the same API. The replacement must be a complete asset root — an index.html and everything it references — because core stops consulting its own assets except as a fallback for paths the replacement does not have.

At most one replacer may be active. Two is a build mistake, not a precedence question: core keeps its own frontend and logs the conflict, because arbitrarily picking one would ship a customer the wrong product.

type HTTPEndpointProvider

type HTTPEndpointProvider interface {
	Extension
	// BasePath is the single path segment this extension owns under /api/ext/.
	// It must be a valid ID segment (lowercase letters, digits, '_' or '-') and
	// unique in the build; a collision is refused rather than resolved.
	BasePath() string
	// Routes returns the endpoints to mount. It is called once, when the API
	// server builds its mux, so the extension must already be provisioned —
	// which it is: the server is built after extensions load.
	Routes() []Route
}

HTTPEndpointProvider is implemented by extensions that expose HTTP endpoints.

Every route is mounted under /api/ext/<BasePath>/, never at an arbitrary path. That boundary is what makes the API surface auditable: core routes and extension routes can never collide, and a reverse proxy can treat the whole extension surface as one prefix.

type HTTPMiddlewareProvider

type HTTPMiddlewareProvider interface {
	Extension
	// Priority orders middleware. Lower priority sits closer to the routes;
	// the highest priority is outermost and sees the request first. Ties break
	// on extension ID so ordering is stable across builds.
	Priority() int
	// WrapHTTP returns a handler wrapping next. Returning next unchanged is
	// valid and means "no opinion for this configuration".
	WrapHTTP(next http.Handler) http.Handler
}

HTTPMiddlewareProvider is implemented by extensions that wrap the whole API handler: authentication, SSO, audit logging, rate limiting, request tagging.

This is the hook an enterprise identity module uses for audit logging, SSO identity propagation, tenant tagging or rate limiting. It sees every request that reaches the API routes — core routes and extension routes alike — and a middleware that rejects a request has genuinely rejected it.

It does *not* replace core's own protection: extension middleware runs inside core's CORS, basic-auth and token checks, so a request that fails those never reaches it. That is deliberate — adding an extension must not be able to weaken the API. Static WebUI assets are served outside this chain too.

type HostServices

type HostServices struct {
	// ID is the extension this value was built for. It is what names the
	// extension in log records and in the environment variables that can
	// override its configuration (see the accessors below). Empty in a host
	// that hands the same value to nobody in particular.
	ID ID

	// Raw is this extension's own configuration subtree, as written under
	// [Extensions.Entries."<id>".Config] in pando.toml. Never nil.
	//
	// Its top-level keys are folded to lower case, because that is what the
	// configuration system does to every key it reads: an author who writes
	// baseURL in a file finds baseurl here. Prefer the typed accessors below,
	// which fold the key being asked for as well and so accept any spelling;
	// index Raw directly only for shapes they do not cover, and then with a
	// lower-case key.
	Raw map[string]any

	// EnvPrefix is the environment-variable prefix used by the configuration
	// accessors. Empty means DefaultConfigEnvPrefix.
	EnvPrefix string

	// Config is a read-only view over the host configuration.
	Config ConfigView

	// Logger is scoped to the extension: records already carry its ID.
	Logger *slog.Logger

	// WorkingDir is the absolute path of the project Pando is running against.
	WorkingDir string

	// CoreVersion is the Pando core version this binary was built from.
	CoreVersion string

	// Variant identifies the build variant ("", "enterprise", ...). Extensions
	// should not branch on it; it exists for reporting.
	Variant string

	// ConfigOverlays lets an extension that implements ConfigOverlayProvider
	// tell the host its overlay document has changed. Nil in hosts that do not
	// support configuration overlays, so check before calling.
	ConfigOverlays ConfigOverlayController

	// Prompts runs a non-interactive prompt through the host's agent. Nil in a
	// host that has no agent, so check before calling.
	Prompts PromptRunner
}

HostServices is the single value handed to every extension on Provision. It carries the extension's own configuration plus the host facilities it is allowed to use.

Every service is an interface declared in this package and satisfied by a core type. That indirection is deliberate: it is what lets core refactor its internals without breaking out-of-tree extensions.

Fields are added as capabilities land (agent, sessions, permissions and the event bus arrive with P1/P2). Adding a field is backwards compatible; removing or retyping one is not.

func (HostServices) Bool

func (h HostServices) Bool(key string, def bool) bool

Bool reads a boolean from the extension's own config subtree.

func (HostServices) ConfigEnvVar added in v0.703.4

func (h HostServices) ConfigEnvVar(key string) string

ConfigEnvVar returns the environment variable that overrides key for this extension: the prefix, "EXT", the extension ID and the key, uppercased with every character that is not a letter or a digit replaced by an underscore. For extension "tools.acme" and key "baseURL" that is PANDO_EXT_TOOLS_ACME_BASEURL.

It returns "" when the host set no extension ID, because there is then no namespace to read an override from.

func (HostServices) Duration added in v0.703.4

func (h HostServices) Duration(key string, def time.Duration) time.Duration

Duration reads a duration from the extension's own config subtree. It accepts a Go duration string ("30s", "5m"), a time.Duration, and a bare number, which is read as seconds because that is what a configuration file most often means by a plain 30.

func (HostServices) Float64 added in v0.703.4

func (h HostServices) Float64(key string, def float64) float64

Float64 reads a floating-point number from the extension's own config subtree.

func (HostServices) Int

func (h HostServices) Int(key string, def int) int

Int reads an integer from the extension's own config subtree. TOML decoding yields int64 and JSON yields float64, so both are accepted, as is a decimal string from the environment.

func (HostServices) Lookup added in v0.703.4

func (h HostServices) Lookup(key string) (any, bool)

Lookup resolves one key of the extension's own configuration subtree.

An environment override wins over the configuration file, matching the precedence the host applies to core settings. Otherwise the key is matched against Raw, first exactly and then case-insensitively. It reports false when neither carries the key.

func (HostServices) Map added in v0.703.4

func (h HostServices) Map(key string) map[string]any

Map reads a nested table from the extension's own config subtree, for an option whose value is itself a set of keys (headers, labels, per-model settings). It returns nil when the key is absent or is not a table.

The returned map's own keys are left exactly as the configuration system produced them: they are data the extension chose to nest, not option names this package may fold.

func (HostServices) String

func (h HostServices) String(key, def string) string

String reads a string from the extension's own config subtree. An empty string is treated as absent, so an option left blank falls back to def.

func (HostServices) StringSlice added in v0.703.4

func (h HostServices) StringSlice(key string, def []string) []string

StringSlice reads a list of strings from the extension's own config subtree. It accepts a TOML or JSON list, and a comma-separated string, which is how a list arrives through the environment. Empty entries are dropped; the result is a copy, so the caller cannot reach into the configuration.

type ID

type ID string

ID uniquely identifies an extension. IDs are namespaced with dots, most general segment first, so that a subsystem can ask for everything under a prefix without knowing concrete types:

tools.acme.jira
api.acme.audit
memory.sink.corp
ui.acme.dashboard

func (ID) Namespace

func (id ID) Namespace() string

Namespace returns everything before the last dot, or "" when the ID has no dot in it.

func (ID) Valid

func (id ID) Valid() bool

Valid reports whether the ID is well formed: non-empty, dot-separated segments of lowercase letters, digits, '_' or '-', with no empty segment.

type Identity added in v0.703.4

type Identity struct {
	// UserID is a stable identifier for the user, opaque to the host. It is
	// the only field the host itself consumes: it becomes the user id on the
	// attribution attached to memory events and session index metadata.
	UserID string

	// Email is the user's address, when the provider knows one. The host does
	// not read, log or persist it; it exists so that a capability which needs
	// it (a sink writing to a store that keys on address) can take it from the
	// same place as everything else rather than growing a second channel.
	Email string

	// DeviceID identifies the machine or enrolment the identity was issued
	// for. Same contract as Email: carried, not consumed.
	DeviceID string

	// Groups lists the group or role names the provider associates with the
	// user. Authorisation is never decided by the host from this list: a
	// permission that matters is enforced by the service that owns the
	// resource, not by the process asking.
	Groups []string
}

Identity is what an identity provider knows about the person the host is running for. Every field is optional; a provider that only knows a stable opaque user id fills UserID and leaves the rest empty.

type IdentityProvider added in v0.703.4

type IdentityProvider interface {
	Extension
	Identity(ctx context.Context) (Identity, bool)
}

IdentityProvider is implemented by extensions that can say who the user is.

The host calls Identity at the moment it needs the answer, never once at startup, so a sign-in or a sign-out that happens while Pando runs takes effect on the next event without a restart. That makes the call latency sensitive: it sits on the path of an ordinary memory write, so it must answer from state the extension already holds and must never block on the network. The host bounds it with a short timeout and contains a panic, but a provider that is routinely slow will be felt.

Returning false means "I do not know right now" — not signed in yet, token expired, enrolment lost. The host then behaves exactly as an unextended Pando does, which is the required behaviour rather than a fallback: an optional capability must never be able to degrade the host that loads it.

When several extensions provide an identity, the first one in load order that returns true wins.

type Info

type Info struct {
	// ID is the namespaced identifier. Required, must be Valid.
	ID ID
	// Name is a short human-readable name.
	Name string
	// Description is one line explaining what the extension does.
	Description string
	// Version is the extension's own version, independent of the core.
	Version string
	// Author identifies who ships it.
	Author string
	// License is informational. Defaults to LicenseMIT when empty.
	License License
	// RequiresCore is an optional semver constraint on the Pando core version
	// (for example ">= 0.647.0"). Empty means no constraint. The manager only
	// records it today; enforcement arrives with the licensing work.
	RequiresCore string
	// RequiresExtensions lists other extension IDs that must be loaded before
	// this one. Load order is derived from it.
	RequiresExtensions []ID
	// New builds a fresh instance. Required. The registry stores the factory,
	// never a live instance, so each Manager gets its own instances.
	New func() Extension
}

Info is the metadata every extension declares. The zero value is invalid: ID and New are required.

func ByNamespace

func ByNamespace(ns string) []Info

ByNamespace returns every extension whose ID equals the namespace or sits under it, sorted by ID. ByNamespace("tools") matches "tools.acme.jira".

func Get

func Get(id ID) (Info, bool)

Get returns the registered Info for an ID.

func List

func List() []Info

List returns every registered extension, sorted by ID.

type License

type License string

License identifies the licensing regime an extension ships under. It is informational: it drives reporting (`pando extensions list`) and, later, entitlement checks. It is not an enforcement mechanism by itself.

const (
	// LicenseMIT is the license of the open-source core and of any extension
	// bundled with it.
	LicenseMIT License = "MIT"
	// LicenseEnterprise marks a closed-source extension shipped only in
	// enterprise builds.
	LicenseEnterprise License = "Enterprise"
)

type LicenseClaims

type LicenseClaims struct {
	// Customer identifies who the license was issued to. Shown in reporting.
	Customer string `json:"customer"`
	// IssuedAt is when the license was minted.
	IssuedAt time.Time `json:"issuedAt,omitzero"`
	// ExpiresAt is when it stops being valid. The zero value means perpetual.
	ExpiresAt time.Time `json:"expiresAt,omitzero"`
	// Entitlements lists what may load.
	Entitlements Entitlements `json:"entitlements"`
	// Notes is free text for humans (contract reference, support tier).
	Notes string `json:"notes,omitempty"`
}

LicenseClaims is the signed payload of a license file.

It is deliberately small. Everything in it is a fact about the customer's agreement, never a runtime setting: a license must not become a second, invisible configuration file.

func VerifyLicense

func VerifyLicense(data []byte, keys map[string]ed25519.PublicKey) (LicenseClaims, error)

VerifyLicense parses and checks a signed license against a set of trusted public keys, keyed by key ID.

It does not check expiry: an expired license still has valid claims worth reporting ("expired on <date>" is a far more useful message than "invalid"), so expiry is left to the gate. Verification failures return the claims' zero value.

func (LicenseClaims) Allows

func (c LicenseClaims) Allows(id ID) bool

Allows reports whether the claims entitle the given extension ID, ignoring expiry. Callers that care about expiry check it separately so they can report "expired" differently from "not covered" — the two need different answers from whoever reads the log.

func (LicenseClaims) Expired

func (c LicenseClaims) Expired(now time.Time) bool

Expired reports whether the license has passed its expiry at the given time. A perpetual license (zero ExpiresAt) never expires.

type LicenseGate

type LicenseGate struct {
	// contains filtered or unexported fields
}

LicenseGate is a ready-made LicenseProvider implementation over a set of verified claims. Enterprise modules embed it rather than reimplementing the entitlement and expiry rules, so that every build answers the same way.

The zero value is not usable; build one with NewLicenseGate or NewUnlicensedGate.

func NewLicenseGate

func NewLicenseGate(claims LicenseClaims, source string) *LicenseGate

NewLicenseGate builds a gate over claims that have already been verified. source describes where they came from, for reporting.

func NewUnlicensedGate

func NewUnlicensedGate(source string, err error) *LicenseGate

NewUnlicensedGate builds a gate for a build that has no usable license: none was found, or it failed to verify. Every non-exempt extension is refused, and the reason travels with the refusal so the log says what actually happened rather than a bare "not licensed".

func (*LicenseGate) Claims

func (g *LicenseGate) Claims() LicenseClaims

Claims exposes the verified claims, for enterprise code that needs a fact from the license beyond entitlement (a support tier, a customer name in a header). It returns a copy of the slice so a caller cannot edit the gate.

func (*LicenseGate) Entitled

func (g *LicenseGate) Entitled(info Info) error

Entitled applies the gate rules: MIT extensions always pass, everything else needs a valid, unexpired license that covers its ID.

func (*LicenseGate) LicenseStatus

func (g *LicenseGate) LicenseStatus() LicenseStatus

LicenseStatus renders the gate's state.

func (*LicenseGate) SetClock

func (g *LicenseGate) SetClock(now func() time.Time)

SetClock replaces the gate's clock. Tests use it; production does not.

type LicenseProvider

type LicenseProvider interface {
	Extension

	// Entitled reports whether the extension described by info may load. A nil
	// error allows it; any error blocks it and is reported verbatim in that
	// extension's Status.
	Entitled(info Info) error

	// LicenseStatus describes the current license for reporting.
	LicenseStatus() LicenseStatus
}

LicenseProvider is implemented by the extension that owns licensing for a build. At most one is expected; if several are compiled in, the manager uses the first in load order and says so.

A LicenseProvider is never gated by its own check — a licensing extension that had to license itself could never start.

type LicenseStatus

type LicenseStatus struct {
	// Present is true when a license document was found at all. False with a
	// non-empty Error means "looked, and could not read one".
	Present bool `json:"present"`
	// Valid is true when the license verified and has not expired.
	Valid bool `json:"valid"`
	// Customer is who it was issued to.
	Customer string `json:"customer,omitempty"`
	// ExpiresAt is the expiry; zero means perpetual.
	ExpiresAt time.Time `json:"expiresAt,omitzero"`
	// Entitlements is what the license grants, for display.
	Entitlements []string `json:"entitlements,omitempty"`
	// Source describes where the license was loaded from.
	Source string `json:"source,omitempty"`
	// Error explains why an otherwise present license is not valid.
	Error string `json:"error,omitempty"`
}

LicenseStatus is what the host reports about licensing: the CLI, the API and the WebUI all render this and nothing else.

It carries no secret: no key material, no file contents, and Source is a description of where the license came from, not necessarily a path.

type Lifecycle

type Lifecycle interface {
	Start(ctx context.Context) error
	Stop(ctx context.Context) error
}

Lifecycle is an optional interface for extensions that run background work. The manager calls Start after all extensions are loaded, and Stop during shutdown before Cleanup. Both must return promptly; long work belongs in a goroutine the extension owns.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager owns the lifecycle of the extensions loaded into this process.

The zero value is not usable; build one with NewManager. A Manager is safe for concurrent use.

func NewManager

func NewManager(opts Options) *Manager

NewManager builds a Manager. It does not load anything: call Load.

func (*Manager) Cleanup

func (m *Manager) Cleanup()

Cleanup unloads every extension in reverse load order. It is safe to call more than once.

func (*Manager) Instance

func (m *Manager) Instance(id ID) Extension

Instance returns the loaded instance of an extension, or nil when it is not loaded.

func (*Manager) LicenseStatus

func (m *Manager) LicenseStatus() (LicenseStatus, bool)

LicenseStatus returns the license state reported by the provider in this build, and whether a provider exists at all. Callers render both cases: "no licensing in this build" and "licensing says X" are different answers, and collapsing them would leave a build with a broken license looking unlicensed by design.

func (*Manager) Load

func (m *Manager) Load(ctx context.Context) error

Load provisions every registered extension that configuration allows, in dependency order.

A failing extension does not abort the others: its error is recorded in its Status and the rest still load. That is deliberate — one broken optional feature must not prevent Pando from starting. Load returns the joined errors so the caller can log them; callers normally do not treat that as fatal.

func (*Manager) Loaded

func (m *Manager) Loaded(id ID) bool

Loaded reports whether an extension is currently loaded.

func (*Manager) Start

func (m *Manager) Start(ctx context.Context) error

Start runs the Lifecycle hook of every loaded extension that has one.

func (*Manager) Statuses

func (m *Manager) Statuses() []Status

Statuses returns the outcome for every registered extension, sorted by ID.

func (*Manager) Stop

func (m *Manager) Stop(ctx context.Context) error

Stop runs the Lifecycle stop hook in reverse load order.

func (*Manager) Unload

func (m *Manager) Unload(id ID) error

Unload stops and cleans up a single extension.

type MemoryEvent

type MemoryEvent struct {
	// Kind distinguishes a keyed memory from a KB document.
	Kind MemoryKind
	// Op is what happened.
	Op MemoryOp
	// Scope is the memory scope ("user/", "project/", "session/", ...). Empty
	// for documents that carry no scope. This is the field the per-scope
	// opt-in gate matches on.
	Scope string
	// Key is the memory key for keyed memories, empty otherwise.
	Key string
	// Path is the document path in the knowledge base. Always set: a keyed
	// memory is stored as a document too.
	Path string
	// Content is the record body. Empty on a delete.
	Content string
	// Tags are the record's tags.
	Tags []string
	// Embedding is the document-level embedding when core already computed
	// one, so a sink need not pay for it again. It may be nil even for a
	// content-bearing event, and a sink must cope with that.
	Embedding []float32
	// Metadata is the record's metadata map, JSON-decodable values only. It is
	// shared with other sinks: treat it as read-only.
	Metadata map[string]any
	// ProjectID identifies the project the write belongs to. Attribution, not
	// an isolation key: isolation belongs to the remote store.
	ProjectID string
	// UserID identifies the instance owner, when known.
	UserID string
	// InstanceID identifies this Pando instance, so a remote store can tell
	// two machines belonging to the same user apart.
	InstanceID string
	// Origin says which subsystem produced the write.
	Origin MemoryOrigin
	// Timestamp is when core observed the write.
	Timestamp time.Time
	// DryRun is true when the host is in dry-run mode. A sink must do
	// everything it normally does *except* send, and log what it would have
	// sent. Honouring this is a hard requirement, not a courtesy: dry-run is
	// how an operator audits what a corporate sink would exfiltrate.
	DryRun bool
}

MemoryEvent is one remembrance write.

The field set is deliberately wide from the first release. Core and the enterprise module ship from two repositories, so adding a field later means a coordinated release of both; carrying a field nobody reads yet costs nothing.

type MemoryKind

type MemoryKind string

MemoryKind distinguishes the two kinds of record the remembrance layer holds. They live in the same table but mean different things: a memory is a short keyed fact with a TTL, a document is durable knowledge-base content.

const (
	// KindMemory is a keyed memory (remember/recall).
	KindMemory MemoryKind = "memory"
	// KindDocument is a knowledge-base document.
	KindDocument MemoryKind = "document"
)

type MemoryOp

type MemoryOp string

MemoryOp is what happened to the record.

const (
	MemoryCreated MemoryOp = "created"
	MemoryUpdated MemoryOp = "updated"
	MemoryDeleted MemoryOp = "deleted"
)

type MemoryOrigin

type MemoryOrigin string

MemoryOrigin says which part of Pando produced the write. A corporate sink needs it to tell a deliberate `remember` from a filesystem mirror sweep that re-imported ten thousand files: the first is worth pushing, the second is usually noise.

const (
	// OriginTool is a write made by the agent through a tool (remember,
	// kb_add_document, ...). The default when nothing sets an origin.
	OriginTool MemoryOrigin = "tool"
	// OriginAPI is a write made through the REST API or the UI.
	OriginAPI MemoryOrigin = "api"
	// OriginSync is a bulk write from the knowledge-base filesystem mirror.
	OriginSync MemoryOrigin = "sync"
	// OriginWatcher is a write triggered by a filesystem change event.
	OriginWatcher MemoryOrigin = "watcher"
	// OriginGC is a write made by the memory garbage collector (expiry,
	// outdated flagging).
	OriginGC MemoryOrigin = "gc"
	// OriginRemote is a write that arrived from another instance or from a
	// remote store. A sink must not push it back out: that is how sync loops
	// are made.
	OriginRemote MemoryOrigin = "remote"
)

type MemorySink

type MemorySink interface {
	Extension
	// OnMemoryWrite receives one write. Never called with a nil event.
	OnMemoryWrite(ctx context.Context, ev MemoryEvent) error
}

MemorySink observes remembrance writes.

OnMemoryWrite is called with a context carrying the host's per-sink timeout. It must return promptly: real work (batching, HTTP, spooling) belongs on a queue the sink owns. Returning an error does not fail or roll back the local write — nothing an extension does can — it is only logged and counted.

Delivery is best effort. When the host queue is full events are dropped, so a sink that must not lose events has to persist them itself, which is what the spool in the corporate sink is for.

type MemorySyncReporter

type MemorySyncReporter interface {
	// MemorySyncStatus returns the sink's current state. Called from HTTP
	// handlers; it must not block.
	MemorySyncStatus() MemorySyncStatus
}

MemorySyncReporter is implemented by a sink that wants its state shown in the UI. The non-negotiable for this capability is that a user can always see, at a glance, that content leaves the machine and where it goes — so a sink that ships data and reports nothing is a bug.

type MemorySyncStatus

type MemorySyncStatus struct {
	// Active reports whether the sink is currently shipping data.
	Active bool `json:"active"`
	// DryRun reports whether it is only pretending to.
	DryRun bool `json:"dryRun"`
	// Destination is human-readable and must identify where data goes (a host
	// name, not a secret-bearing URL).
	Destination string `json:"destination,omitempty"`
	// Scopes lists the scopes this sink is allowed to ship.
	Scopes []string `json:"scopes,omitempty"`
	// Pending is the number of events waiting to be sent.
	Pending int `json:"pending"`
	// Sent is the number of events shipped since start.
	Sent int64 `json:"sent"`
	// Dropped is the number of events lost, for any reason.
	Dropped int64 `json:"dropped"`
	// LastSyncAt is when the last successful send completed.
	LastSyncAt time.Time `json:"lastSyncAt,omitzero"`
	// LastError is the last failure, empty when healthy.
	LastError string `json:"lastError,omitempty"`
}

MemorySyncStatus is what the UI shows about one sink.

type Options

type Options struct {
	// Registry to load from. Defaults to the package-level registry.
	Registry *Registry
	// Entries is the per-extension configuration, keyed by ID.
	Entries map[string]Entry
	// Disabled lists IDs that must never load, whatever Entries says. It is the
	// stronger switch: it also turns off extensions that would otherwise load
	// by default.
	Disabled []string
	// Host is the base HostServices handed to each extension. The manager fills
	// in Raw and Logger per extension; the rest is passed through untouched.
	Host HostServices
	// Logger receives manager-level messages. Defaults to slog.Default().
	Logger *slog.Logger
}

Options configures a Manager.

type PanelManifest

type PanelManifest struct {
	// ID is unique within the extension. Core namespaces it with the extension
	// ID before handing it to the shell, so two extensions may use the same one.
	ID string
	// Title is the label shown to the user.
	Title string
	// Slot is where the panel mounts. Use one of the Slot* constants; an
	// unrecognised value is dropped with a log line rather than guessed at.
	Slot string
	// Entry is the ES module entry point, relative to the extension's asset
	// root and without a leading slash: "panels/reports.js". Core turns it into
	// an absolute URL for the shell.
	Entry string
	// Icon is an optional icon name the shell understands.
	Icon string
	// Order sorts panels inside a slot; equal values fall back to extension ID.
	Order int
}

PanelManifest describes one panel an extension contributes to the core WebUI.

The shell fetches the merged manifest at boot and dynamically imports each Entry as an ES module. Panels are additive: an extension that only wants to re-skin the product wants FrontendOverlay or FrontendReplacer instead.

type PromptProgress added in v0.703.4

type PromptProgress struct {
	// SessionID is the session the turn runs in.
	SessionID string
	// Delta is newly produced assistant text.
	Delta string
	// ToolName names a tool the turn has just started running.
	ToolName string
	// PromptTokens and CompletionTokens are the turn's running token totals.
	PromptTokens     int64
	CompletionTokens int64
	// CostUSD is the run's cost so far, in US dollars, as the host computed it
	// from the model's published prices. Zero when the host cannot price the
	// model.
	CostUSD float64
}

PromptProgress is one update from a running turn. Exactly one of Delta and ToolName is set on any given update; the usage numbers are running totals and are zero until the host has confirmed any.

type PromptRequest added in v0.703.4

type PromptRequest struct {
	// Prompt is the user message. Required.
	Prompt string

	// Title names the session created for the run, for the session list and
	// the logs. Defaults to a truncation of the prompt.
	Title string

	// SessionID continues an existing session instead of creating one. Empty
	// starts a fresh session, which is what a one-shot job wants.
	SessionID string

	// AutoApprove answers every permission request for this run with yes.
	//
	// It exists because nobody is watching: a run started by a schedule or a
	// pipeline has no human to answer a prompt, and a turn that blocks forever
	// on an unanswerable question is worse than one that was told in advance
	// what it may do. It applies to this run's session only, never globally,
	// and an extension that leaves it false gets a turn that stops at the
	// first permission request.
	AutoApprove bool

	// OnProgress, when set, is called as the turn produces output. It is
	// called from the host's event goroutine and must return promptly: slow
	// work belongs on a queue the extension owns. A panic in it is contained
	// and the run continues.
	OnProgress func(PromptProgress)
}

PromptRequest is one turn to run.

type PromptResult added in v0.703.4

type PromptResult struct {
	// SessionID is the session the turn ran in, whether it was created for the
	// run or continued.
	SessionID string
	// Text is the assistant's final answer.
	Text string
	// FinishReason is the host's reason for ending the turn ("end_turn",
	// "max_tokens", "canceled", "permission_denied", ...). It is a string
	// rather than an enumeration because the set grows with the providers.
	FinishReason string
	// PromptTokens and CompletionTokens are the turn's token totals.
	PromptTokens     int64
	CompletionTokens int64
	// CostUSD is what the turn cost, in US dollars, or zero when the host
	// cannot price the model.
	CostUSD float64
}

PromptResult is the outcome of a turn.

type PromptRunner added in v0.703.4

type PromptRunner interface {
	// RunPrompt sends req and blocks until the turn ends, the context is
	// cancelled, or the agent fails. A cancelled context ends the turn and
	// returns what was produced so far together with ctx.Err().
	RunPrompt(ctx context.Context, req PromptRequest) (PromptResult, error)
}

PromptRunner runs one prompt through the host's agent and returns its answer. It is offered on HostServices.Prompts and is nil in a host with no agent (a CLI subcommand that never built one), so check before calling.

Calls are not serialised by the host: an extension that must not run two prompts at once serialises them itself.

type ProviderRequest added in v0.703.4

type ProviderRequest struct {
	// Provider is the provider identifier the host resolved for this call
	// ("anthropic", "openai", "ollama", ...).
	Provider string

	// Model is the API model id the request will name.
	Model string
}

ProviderRequest describes the outgoing call a decorator is being asked about. It is deliberately thin: it names the destination, not the payload. A decorator that wants to know more about the work in flight reads it from the context, which is the caller's own request context.

type ProviderRequestDecorator added in v0.703.4

type ProviderRequestDecorator interface {
	Extension
	DecorateProviderRequest(ctx context.Context, req ProviderRequest) (map[string]string, error)
}

ProviderRequestDecorator is implemented by extensions that add headers to outgoing provider requests.

DecorateProviderRequest is called once per HTTP request, on the goroutine making it, with that request's context. It must be cheap and must not block: it sits directly in front of a network call the user is waiting on.

The returned map is a set of header names and values to add. The host applies them defensively:

  • Security-relevant and transport-owned headers are never replaceable. A decorator cannot set or overwrite the credential headers, the cookie headers, the provider's own protocol headers, or the framing headers; entries naming one are dropped and logged. This is enforced by the host rather than promised by the contract, because a contract is not a boundary.
  • An empty name, or an error, means "add nothing to this request". An error is logged and the request proceeds unchanged; it never fails the call, because an optional capability must not be able to break the host's ability to talk to its provider.

With no decorator registered the host sends exactly the bytes it sent before this capability existed.

type Provisioner

type Provisioner interface {
	Provision(ctx context.Context, host HostServices) error
}

Provisioner is implemented by extensions that need setup once their configuration and the host services are available. Returning an error aborts loading of that extension.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry holds extension factories keyed by ID. Extensions register into the package-level default registry from init(); a separate Registry is mainly useful in tests, which must not see whatever the rest of the binary registered.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty registry.

func (*Registry) ByNamespace

func (r *Registry) ByNamespace(ns string) []Info

ByNamespace returns every extension whose ID equals the namespace or sits under it, sorted by ID.

func (*Registry) Get

func (r *Registry) Get(id ID) (Info, bool)

Get returns the registered Info for an ID.

func (*Registry) Len

func (r *Registry) Len() int

Len reports how many extensions are registered.

func (*Registry) List

func (r *Registry) List() []Info

List returns every registered extension, sorted by ID.

func (*Registry) Register

func (r *Registry) Register(e Extension)

Register adds an extension to this registry. See the package-level Register for the panic contract.

type Remembrance

type Remembrance struct {
	// Path identifies the record.
	Path string
	// Content is the matching text.
	Content string
	// Score is the relevance score, higher is better. Scores from different
	// stores are not comparable; a wrapper that merges two stores is
	// responsible for producing a ranking that makes sense.
	Score float64
	// Tags are the record's tags.
	Tags []string
	// Scope is the record's scope, when it has one.
	Scope string
	// Source names where the hit came from: empty (or "local") for core's own
	// store, otherwise a wrapper-supplied label shown to the user. A merged
	// result set that does not label its remote hits is indistinguishable from
	// a local one, which is exactly what the visibility requirement forbids.
	Source string
	// UpdatedAt is the record's last modification time, when known.
	UpdatedAt time.Time
}

Remembrance is one search hit in the neutral form.

type RemembranceQuery

type RemembranceQuery struct {
	// Query is the natural-language search text.
	Query string
	// Limit is the maximum number of results the caller wants.
	Limit int
	// Tags narrows the search to records carrying any of these tags.
	Tags []string
	// Scope narrows the search to one scope prefix. Empty means every scope.
	Scope string
	// ProjectID is the project the search runs in.
	ProjectID string
}

RemembranceQuery is a search of the remembrance layer, in the neutral form a wrapper sees it.

type RemembranceSearchWrapper

type RemembranceSearchWrapper interface {
	Extension
	// WrapRemembranceSearch is called once, during startup, before any search
	// runs. It must not block.
	WrapRemembranceSearch(next RemembranceSearcher) RemembranceSearcher
}

RemembranceSearchWrapper decorates remembrance search: the classic storage decorator, applied once at startup.

The wrapper receives the next searcher in the chain and returns the one that replaces it. Chaining runs in registration order, so the first registered extension ends up outermost. A wrapper that decides not to act must return next unchanged.

A wrapper that fails should fall back to next rather than return an error: a corporate store being unreachable must degrade to local-only search, never break the agent's ability to recall anything at all.

type RemembranceSearcher

type RemembranceSearcher interface {
	SearchRemembrances(ctx context.Context, q RemembranceQuery) ([]Remembrance, error)
}

RemembranceSearcher is core's search, in the neutral form. A wrapper receives one and returns one.

type RemembranceSearcherFunc

type RemembranceSearcherFunc func(ctx context.Context, q RemembranceQuery) ([]Remembrance, error)

RemembranceSearcherFunc adapts a function to RemembranceSearcher.

func (RemembranceSearcherFunc) SearchRemembrances

func (f RemembranceSearcherFunc) SearchRemembrances(ctx context.Context, q RemembranceQuery) ([]Remembrance, error)

type Route

type Route struct {
	// Pattern is a net/http ServeMux pattern *relative to the extension's base
	// path*, without a leading slash: "ping", "reports/{id}", "GET /status".
	// A method prefix is honoured exactly as ServeMux honours it.
	Pattern string
	// Handler serves the route.
	Handler http.Handler
}

Route is one HTTP endpoint contributed by an extension.

type SignedLicense

type SignedLicense struct {
	// KeyID names the signing key, so keys can be rotated without breaking
	// licenses already issued under the previous one.
	KeyID string `json:"keyId"`
	// Claims is the raw JSON of the LicenseClaims.
	Claims json.RawMessage `json:"claims"`
	// Signature is the base64 (standard encoding) Ed25519 signature over Claims.
	Signature string `json:"signature"`
}

SignedLicense is the on-disk envelope: a JSON document with the claims kept as raw bytes.

The signature covers those bytes with insignificant whitespace removed (encoding/json's Compact, nothing more). Compaction is the only normalisation applied: key order, values and unknown fields are all left exactly as the issuer wrote them, so a field added by a newer issuer does not invalidate an older reader's check. Whitespace has to be normalised because writing an indented, human-readable license file necessarily re-indents the claims.

type SlashCommand

type SlashCommand struct {
	// Name is typed after the slash, lowercase, no spaces. It must not collide
	// with a built-in command; registration rejects the collision rather than
	// letting an extension hijack /compact.
	Name string
	// Description is shown in the command palette and completions.
	Description string
	// AcceptsArgs tells the UI whether to expect text after the name.
	AcceptsArgs bool
}

SlashCommand is one command a user can type in a session as "/name args".

type SlashCommandProvider

type SlashCommandProvider interface {
	Extension
	SlashCommands() []SlashCommand
	// RunSlashCommand executes one of the declared commands. args is the raw
	// text after the command name, untrimmed of internal spacing. Returning an
	// error surfaces it to the user; it does not abort the session.
	RunSlashCommand(ctx context.Context, name, args string) (SlashResult, error)
}

SlashCommandProvider is implemented by extensions that add slash commands. The same extension must both declare and execute them: core routes by name.

type SlashResult

type SlashResult struct {
	Prompt string
	Output string
}

SlashResult is what running a slash command produced. Exactly one of Prompt and Output is normally set:

  • Prompt is sent to the model as if the user had typed it, which is how prompt-expanding commands (the /vulnhunt family, custom .md commands) work.
  • Output is shown to the user directly and starts no model turn, which is how state-changing commands (/caveman, /goal-status) work.

Setting both shows Output and then runs Prompt. Setting neither is a no-op, which is the right result for a command that only changed hidden state.

type Status

type Status struct {
	Info Info
	// Loaded is true once Provision and Validate have both succeeded.
	Loaded bool
	// Disabled is true when configuration switched the extension off; Err is
	// nil in that case.
	Disabled bool
	// Unlicensed is true when the license gate refused the extension. Err then
	// holds the reason. It is kept apart from a plain load failure because the
	// two need different answers: a load failure is a bug report, an
	// unlicensed extension is a question for whoever owns the contract.
	Unlicensed bool
	// Err holds the reason an extension failed to load.
	Err error
}

Status describes what happened to one extension in a Manager.

func (Status) String

func (s Status) String() string

String renders a one-line status, used by `pando extensions list`.

type Tool

type Tool interface {
	Info() ToolInfo
	Run(ctx context.Context, call ToolCall) (ToolResponse, error)
}

Tool is a single tool contributed by an extension.

type ToolCall

type ToolCall struct {
	// ID is the provider-assigned call identifier.
	ID string
	// Name is the tool being called.
	Name string
	// Input is the raw JSON argument object.
	Input string
}

ToolCall is one invocation of a tool by the model.

type ToolFilter

type ToolFilter interface {
	ToolMiddleware
	FilterTools(tools []Tool) []Tool
}

ToolFilter rewrites the tool list before it is offered to the model. It may drop, reorder or wrap tools, and it must return a slice — returning nil removes every tool, which is a valid (if drastic) policy.

Filters run once per tool-set build, not per call.

type ToolFunc

type ToolFunc func(ctx context.Context, call ToolCall) (ToolResponse, error)

ToolFunc is the next link in an interceptor chain.

type ToolInfo

type ToolInfo struct {
	// Name is the tool name the model calls. Must be unique across all tools in
	// a build; namespace it with a vendor prefix to avoid clashing with core.
	Name string
	// Description tells the model what the tool does and when to use it.
	Description string
	// Parameters is a JSON Schema "properties" object describing the input.
	Parameters map[string]any
	// Required lists the required parameter names.
	Required []string
}

ToolInfo describes a tool to the model.

type ToolInterceptor

type ToolInterceptor interface {
	ToolMiddleware
	InterceptTool(ctx context.Context, call ToolCall, next ToolFunc) (ToolResponse, error)
}

ToolInterceptor wraps the execution of every tool call, core tools included. The implementation must call next exactly once unless it is deliberately refusing the call, in which case it returns a ToolResponse with IsError set and never calls next.

Interceptors are the audit/redaction/quota hook. Returning a Go error aborts the call; prefer an error ToolResponse so the model can react.

type ToolMiddleware

type ToolMiddleware interface {
	Extension
	// Priority orders middleware. Lower runs closer to the tool: filters with a
	// lower priority run first, and interceptors with a lower priority sit
	// innermost, so a high-priority interceptor observes the calls a
	// low-priority one made. Ties break on extension ID, so ordering is stable
	// across builds.
	Priority() int
}

ToolMiddleware is the base interface for extensions that observe or alter the agent's tool set. It carries only the ordering rule; the actual work is declared by ToolFilter, ToolInterceptor, or both.

Middleware sees *every* tool, core tools included, not just the tools contributed by extensions. That is the point: an enterprise policy module exists to constrain what the model can reach.

type ToolProvider

type ToolProvider interface {
	Extension
	// Tools returns the tools to register. It is called once per agent build,
	// so it may return different tools as configuration changes.
	Tools() []Tool
}

ToolProvider is implemented by extensions that add tools to the agent.

type ToolResponse

type ToolResponse struct {
	// Content is the textual result.
	Content string
	// Metadata is an optional JSON object carried alongside the content, shown
	// in the UI but not necessarily sent to the model.
	Metadata string
	// IsError marks the call as failed. Prefer returning a ToolResponse with
	// IsError set over returning a Go error: the former is reported to the
	// model, the latter aborts the call.
	IsError bool
}

ToolResponse is the result handed back to the model.

func NewErrorResponse

func NewErrorResponse(content string) ToolResponse

NewErrorResponse builds a failed response the model can read and react to.

func NewTextResponse

func NewTextResponse(content string) ToolResponse

NewTextResponse builds a successful textual response.

type UIPolicy added in v0.703.4

type UIPolicy struct {
	// HiddenSections lists the paths the surfaces should not render at all. A
	// section whose every field is hidden disappears with them.
	//
	// The host also refuses local writes to a hidden path, so that hiding is
	// not the only thing standing between a client and the value.
	HiddenSections []string

	// ReadOnlySections lists the paths the surfaces should render with their
	// value visible but not editable, marked with ReadOnlyLabel. Writes to
	// them are refused for the same reason as hidden paths.
	//
	// A path that is in both lists is hidden: the stronger statement wins.
	ReadOnlySections []string

	// ReadOnlyLabel is the short caller-supplied text a surface shows on a
	// read-only field, in place of the host's own generic marker ("Managed by
	// the operator", the name of the system that owns the value). Optional;
	// the host uses its default marker when it is empty.
	ReadOnlyLabel string

	// Banner is the notice the surfaces show above the settings, explaining
	// once what the rest of the policy does to individual fields. Optional.
	Banner UIPolicyBanner
}

UIPolicy is what a UI policy provider asks the host's settings surfaces to do. The zero value asks for nothing, which is exactly how an unextended Pando behaves.

Paths in HiddenSections and ReadOnlySections are dotted paths into the configuration document ("providerAccounts", "internalTools.braveApiKey"), matched case-insensitively segment by segment, the same way ConfigOverlay treats Locked. A path names either a leaf or a whole subtree: hiding "mcpServers" hides every server under it.

func (UIPolicy) Empty added in v0.703.4

func (p UIPolicy) Empty() bool

Empty reports whether the policy asks for nothing at all.

type UIPolicyBanner added in v0.703.4

type UIPolicyBanner struct {
	// Text is the message. Keep it to one line: the terminal renders it as a
	// single row above the sections.
	Text string

	// Link is an optional URL the message refers to, rendered as a plain URL
	// on surfaces that cannot make it clickable.
	Link string
}

UIPolicyBanner is the notice a surface renders above its settings.

type UIPolicyProvider added in v0.703.4

type UIPolicyProvider interface {
	Extension
	UIPolicy(ctx context.Context) (UIPolicy, bool)
}

UIPolicyProvider is implemented by extensions that shape the host's settings surfaces.

The host asks at render time, never once at start-up, so a policy that appears or disappears while Pando runs (an enrolment completing, a session ending) is reflected the next time a surface is drawn without a restart. The call must therefore answer from state the extension already holds and must never block on the network; the host bounds it with a short timeout and contains a panic, and treats either as "no policy".

Returning false means "nothing to say right now", which leaves every surface behaving exactly as an unextended Pando. That is the required behaviour rather than a fallback: an optional capability must never be able to degrade the host that loads it.

When several extensions provide a policy, the host merges them: the hidden and read-only sets are unioned, so any provider can hide a path and none can unhide one, while the single-valued ReadOnlyLabel and Banner are taken from the first provider in load order that sets them and later ones are dropped. A policy is a restriction; merging it can only ever restrict further.

type Validator

type Validator interface {
	Validate() error
}

Validator is implemented by extensions that can reject their own configuration. It runs after Provision; a failure triggers Cleanup.

Jump to

Keyboard shortcuts

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