extend

package
v1.0.2 Latest Latest
Warning

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

Go to latest
Published: Jun 23, 2026 License: AGPL-3.0 Imports: 9 Imported by: 0

Documentation

Overview

Package extend is the hook registry that lets installed apps modify the daemon's existing primitives. Apps declare their hooks in the manifest `extends` field; at app-start the runtime registers them here; daemon primitives consult the registry and run the hook chain inline.

The model: a primitive (send-message, recv, net.call, ...) is a sequence of hook chains around its core logic — PreX, core, PostX. Each hook is an IPC call to an installed app. The app may transform the HookArgs (the request/response payload) before returning. Later hooks see the transformed args, so apps stack predictably.

Hooks are not authority-bearing — they cannot bypass the daemon's broker; they cannot grant themselves permissions; the daemon's existing safety floor (grants, deny-by-default) applies to every IPC call the hook makes back into the broker. A hook is a *transformer*, not a privileged kernel module.

Index

Constants

This section is empty.

Variables

View Source
var ErrPermissionDenied = errors.New("extend: permission denied")

ErrPermissionDenied is returned by DaemonHandler.Register when the configured Permission rejects the request.

View Source
var ErrRateLimited = errors.New("extend: hook rate limit exceeded")

ErrRateLimited is returned by Run when an app's hook-invocation rate budget is exhausted. The chain aborts rather than dispatch into an app that's being called too aggressively.

View Source
var ErrTooManyRegistrations = errors.New("extend: too many dynamic registrations for app")

ErrTooManyRegistrations is returned by DaemonHandler.Register when an app already holds maxDynamicRegistrationsPerApp hooks.

Functions

func AddEncoding

func AddEncoding(args HookArgs, codings ...string)

AddEncoding contributes one or more encoding labels (in order) to WireMeta.Encoding. Mirrors HTTP Content-Encoding chaining: the last label is the outermost transform, applied first by senders and reversed last by receivers.

func AddRequired

func AddRequired(args HookArgs, appIDs ...string)

AddRequired is a hook-side helper that contributes appIDs to the cumulative WireMeta.Required list. The framework de-duplicates.

func IsValid

func IsValid(s string) bool

IsValid reports whether s has the form "<command>.<phase>". Caller is expected to use this when accepting hook points from an untrusted source (e.g. parsing a manifest). The Registry calls it itself on Register, so apps cannot smuggle malformed points into the runtime.

func MissingRequired

func MissingRequired(meta *WireMeta, installed []string) []string

MissingRequired returns the apps in meta.Required that aren't in installed. Recipient daemons call this on incoming messages before post-recv dispatch — if non-empty, the daemon surfaces the missing apps to the user instead of trying to decode.

func SetDetails

func SetDetails(args HookArgs, details map[string]any)

SetDetails is a hook-side helper that contributes Details to the stamp the registry will append after this hook returns. Call inside the hook implementation, just before returning args.

func SetMeta

func SetMeta(args HookArgs, m *WireMeta)

SetMeta writes m back to args. Pass nil to clear.

Types

type Core

type Core func(ctx context.Context, args HookArgs) (HookArgs, error)

Core is the type a command's baseline logic takes. Wrap calls it between the pre-hook chain and the post-hook chain; the args it receives are the pre-transformed args, and whatever it returns flows into the post chain.

type DaemonHandler

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

DaemonHandler is the runtime-side IPC surface the daemon exposes to installed apps so they can add/remove their own hooks dynamically (within the bounds Permission allows). Apps call these methods via the daemon's normal IPC broker — the broker enforces the per-app `ipc.call:extend.register` grant first; if that passes, the call lands here, and Permission gates which hook points are reachable.

func NewDaemonHandler

func NewDaemonHandler(reg *Registry, perms Permission) *DaemonHandler

NewDaemonHandler returns a handler bound to a Registry. If perms is nil, DenyAll is used.

func (*DaemonHandler) List

func (h *DaemonHandler) List(primitive HookPoint) []Extension

List returns all currently-registered extensions, optionally filtered by primitive. Used for introspection (pilotctl extend list).

func (*DaemonHandler) Register

func (h *DaemonHandler) Register(appID string, ext Extension) error

Register is called by apps to install a hook at runtime. AppID is set by the daemon's broker from the calling app's identity, not by the caller — the app can't impersonate someone else.

func (*DaemonHandler) Unregister

func (h *DaemonHandler) Unregister(appID string, primitive HookPoint, method string) error

Unregister removes a previously-registered hook. Apps may only unregister their own hooks; the (appID, primitive, method) triple is the unique key.

type Dispatcher

type Dispatcher func(ctx context.Context, appID, method string, args HookArgs) (HookArgs, error)

Dispatcher is how the registry calls an app's hook method. The app-store plugin wires this to its IPC client pool; tests inject a closure that runs hooks in-process.

type Extension

type Extension struct {
	AppID     string
	Primitive HookPoint
	Method    string     // IPC method name on the app, e.g. "wallet.hookPreSendMessage"
	AddsFlags []FlagSpec // flags this hook contributes to its primitive's CLI
	Order     int        // lower runs earlier in the chain
	Version   string     // optional version string recorded in HookStamp
}

Extension is one registered hook. AppID + Method together name the IPC endpoint the registry dispatches to.

type FlagSpec

type FlagSpec struct {
	Name string `json:"name"` // "--paywall" — must include leading dashes
	Type string `json:"type"` // "string" | "bool" | "int"
	Help string `json:"help,omitempty"`
}

FlagSpec describes one CLI flag an app contributes to a primitive via a hook. Pilotctl asks the registry for all flags contributed at a given hook point before parsing args, so apps shape the CLI surface.

type HookArgs

type HookArgs map[string]any

HookArgs is the request/response payload passed through a hook chain. Generic map for now; per-hook-point schemas are documented in the HookPoint constants above. JSON-serializable.

func Wrap

func Wrap(ctx context.Context, r *Registry, cmd string, args HookArgs, core Core) (HookArgs, error)

Wrap is the single generic wrapper that gives every command pre and post hooks. The daemon (and pilotctl) dispatch every command through Wrap; the command author never opts in or out — the wrapping is uniform across the platform.

Hooks extend the command's functionality, they never replace it. Pre-hooks may transform args or refuse (return error → abort, core does not run). Post-hooks see the core's output. Neither can bypass the core.

type HookPoint

type HookPoint string

HookPoint identifies one extensible step in a command's execution. Format: "<command>.<phase>" where:

  • <command> is any reverse-DNS-ish name ("send-message", "wallet.pay", "appstore.install", "memories.recall", ...). Daemon built-ins, app-defined commands, and pilotctl subcommands all use the same namespace — anything the runtime wraps with Registry.Run is a hook point.

  • <phase> is one of "pre" (runs before the core; may transform args or abort with an error) or "post" (runs after the core; sees the result; cannot un-run).

The command space is open — apps can register hooks against commands no daemon-or-app wraps yet; those hooks are inert until something wraps the point. This lets apps anticipate future extension targets.

const (
	PreSendMessage  HookPoint = "send-message.pre"
	PostSendMessage HookPoint = "send-message.post"
	PostRecvMessage HookPoint = "recv.post"
	PreNetCall      HookPoint = "net.call.pre"
	PostNetCall     HookPoint = "net.call.post"
)

Common built-in hook points the daemon wraps. These are just well-known string values — there is no whitelist enforcement; any shape-valid HookPoint is accepted.

type HookStamp

type HookStamp struct {
	AppID     string         `json:"app"`
	Primitive string         `json:"primitive"`
	Version   string         `json:"version,omitempty"`
	At        time.Time      `json:"at"`
	Details   map[string]any `json:"details,omitempty"`
}

HookStamp is one entry in the trail. App and Primitive are recorded by the registry — the hook can't lie about them. Details is the only part the hook controls; it's where the wallet writes its contract_id, where the compressor writes its algorithm, etc.

type Permission

type Permission interface {
	CanRegister(appID string, primitive HookPoint) bool
}

Permission is the gate the daemon's runtime-register IPC consults before adding a hook to the registry on behalf of an app. The default implementation in the appstore plugin grants a (appID, primitive) pair only if the manifest's `dynamic_extends` list includes primitive — i.e. the app declared at install time that it might register this point later.

Daemons / tests may swap in stricter policies (per-user prompt, allowlist file, …) without touching extend's wiring.

var AllowAll Permission = PermissionFunc(func(string, HookPoint) bool { return true })

AllowAll is a permission that grants any registration. Convenient for tests; never use as production default.

var DenyAll Permission = PermissionFunc(func(string, HookPoint) bool { return false })

DenyAll rejects every registration. Useful as a safe default before a real Permission is wired.

type PermissionFunc

type PermissionFunc func(appID string, primitive HookPoint) bool

PermissionFunc adapts an ordinary function to the Permission interface.

func (PermissionFunc) CanRegister

func (f PermissionFunc) CanRegister(appID string, primitive HookPoint) bool

CanRegister forwards to the wrapped function.

type Phase

type Phase string

Phase enumerates the two universal hook positions around a command's core logic. The command name is free, the phase is not.

const (
	PhasePre  Phase = "pre"
	PhasePost Phase = "post"
)

type Registry

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

Registry holds all registered extensions. Safe for concurrent reads after Register/Unregister; the dispatch path takes only a read lock.

func NewRegistry

func NewRegistry(dispatch Dispatcher) *Registry

NewRegistry constructs an empty Registry. dispatch is required.

func (*Registry) AllRegistered

func (r *Registry) AllRegistered() []Extension

AllRegistered returns every registered extension across every hook point. Used by the runtime-register IPC's List method for introspection.

func (*Registry) CountForApp added in v1.0.1

func (r *Registry) CountForApp(appID string) int

CountForApp returns the number of currently-registered extensions belonging to appID across all hook points. Used to bound how many dynamic registrations a single app may hold.

func (*Registry) FlagsFor

func (r *Registry) FlagsFor(p HookPoint) []FlagSpec

FlagsFor returns the union of flag specs contributed at a hook point. Pilotctl uses this to extend its CLI surface dynamically. Duplicate flag names across apps are returned as duplicates — the caller should detect collisions and decide a resolution.

func (*Registry) HooksFor

func (r *Registry) HooksFor(p HookPoint) []Extension

HooksFor returns the registered extensions for a hook point, in run order. Safe to call concurrently.

func (*Registry) Register

func (r *Registry) Register(ext Extension) error

Register adds one extension to the registry. Returns an error if the primitive is shape-invalid, the method is empty, or a flag is shaped wrong. Multiple apps may register for the same hook point; the point itself does not need to be "known" — registering against an unwrapped point is allowed and inert until something wraps it.

func (*Registry) Run

func (r *Registry) Run(ctx context.Context, p HookPoint, args HookArgs) (HookArgs, error)

Run executes the hook chain for a point, threading args through each hook in order. Any hook returning an error aborts the chain and the caller's primitive call.

args is treated as mutable — the hook may modify in place — but for safety the registry passes a shallow copy on each call so a misbehaving hook can't mutate args from under a later hook unexpectedly.

After each successful hook return, the registry appends a HookStamp to args["__meta"].TouchedBy. The stamp's AppID + Primitive come from the registry (the hook can't lie about who it is); Details and Required are read from scratch keys (__meta_details, __meta_required) the hook may have populated, then cleared.

func (*Registry) SetRateLimit added in v1.0.1

func (r *Registry) SetRateLimit(ratePerSec float64, burst int)

SetRateLimit enables per-app hook-dispatch rate limiting: each app may fire `burst` hook invocations instantly and `ratePerSec` sustained thereafter. Once an app's budget is exhausted, Run aborts that app's hook with ErrRateLimited. Call with a non-positive rate or burst to disable. Intended to be set once at daemon wire-up.

func (*Registry) Unregister

func (r *Registry) Unregister(appID string)

Unregister removes all hooks belonging to appID. Called when an app is uninstalled or suspended. Idempotent.

func (*Registry) UnregisterOne

func (r *Registry) UnregisterOne(appID string, primitive HookPoint, method string)

UnregisterOne removes a specific (appID, primitive, method) triple. Used by the runtime-register IPC surface to undo a single dynamic registration without dropping the rest of an app's hooks.

type WireMeta

type WireMeta struct {
	TouchedBy []HookStamp `json:"touched_by,omitempty"`
	Required  []string    `json:"required,omitempty"`
	Encoding  []string    `json:"encoding,omitempty"`
}

WireMeta is the provenance trail that follows a hooked message on the wire. The Wrap helper populates it as hooks run; the daemon serializes it into the outbound envelope; the recipient parses it before attempting to decode the body. If any app in Required is not locally installed, the recipient surfaces that to the user instead of failing to decode.

Cumulative across the whole hook chain. Receivers can also append their own stamps via post-recv hooks, producing a two-sided record of every app that touched the message.

func GetMeta

func GetMeta(args HookArgs) *WireMeta

GetMeta extracts the WireMeta from args. Returns an empty (non-nil) WireMeta if absent, so callers can safely append without nil checks.

Jump to

Keyboard shortcuts

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