extension

package
v0.700.1 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: MIT Imports: 16 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"
)

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.

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

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

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 {
	// Raw is this extension's own configuration subtree, as written under
	// [Extensions.Entries."<id>".Config] in pando.toml. Never nil.
	Raw map[string]any

	// 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
}

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

func (HostServices) String

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

String reads a string from the extension's own config subtree.

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