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 ¶
- Variables
- func AddEncoding(args HookArgs, codings ...string)
- func AddRequired(args HookArgs, appIDs ...string)
- func IsValid(s string) bool
- func MissingRequired(meta *WireMeta, installed []string) []string
- func SetDetails(args HookArgs, details map[string]any)
- func SetMeta(args HookArgs, m *WireMeta)
- type Core
- type DaemonHandler
- type Dispatcher
- type Extension
- type FlagSpec
- type HookArgs
- type HookPoint
- type HookStamp
- type Permission
- type PermissionFunc
- type Phase
- type Registry
- func (r *Registry) AllRegistered() []Extension
- func (r *Registry) FlagsFor(p HookPoint) []FlagSpec
- func (r *Registry) HooksFor(p HookPoint) []Extension
- func (r *Registry) Register(ext Extension) error
- func (r *Registry) Run(ctx context.Context, p HookPoint, args HookArgs) (HookArgs, error)
- func (r *Registry) Unregister(appID string)
- func (r *Registry) UnregisterOne(appID string, primitive HookPoint, method string)
- type WireMeta
Constants ¶
This section is empty.
Variables ¶
var ErrPermissionDenied = errors.New("extend: permission denied")
ErrPermissionDenied is returned by DaemonHandler.Register when the configured Permission rejects the request.
Functions ¶
func AddEncoding ¶
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 ¶
AddRequired is a hook-side helper that contributes appIDs to the cumulative WireMeta.Required list. The framework de-duplicates.
func IsValid ¶
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 ¶
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 ¶
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.
Types ¶
type Core ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
AllRegistered returns every registered extension across every hook point. Used by the runtime-register IPC's List method for introspection.
func (*Registry) FlagsFor ¶
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 ¶
HooksFor returns the registered extensions for a hook point, in run order. Safe to call concurrently.
func (*Registry) Register ¶
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 ¶
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) Unregister ¶
Unregister removes all hooks belonging to appID. Called when an app is uninstalled or suspended. Idempotent.
func (*Registry) UnregisterOne ¶
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.