Documentation
¶
Overview ¶
Package pluginreq is the "leave a request" half of installing a plugin.
The rule (owner, 2026-09-28) ¶
An API key — an agent, a script, the CLI — may not install, upgrade, remove or re-permission a plugin. Those need an administrator signed in to the admin panel (handlers.requireSession). What a key CAN do is leave a REQUEST: which plugin, from where, and why. An administrator reads it on the Plugins page and approves or rejects it.
The design principle it protects: "sha256 protects the administrator, the sandbox protects the user". The security boundary for an app is the administrator's permission review; before this, a key could copy the manifest's permission list into `permissions` and pass that review itself.
What a request freezes ¶
A request is resolved when it is made — the install review's dry run — and what the source answered is kept on the row: the manifest (an app's filex-app.json, a storage plugin's filex-storage.json feed), the hash the bytes must have, and the permissions it asks to grant. The administrator approves THAT. Approval fetches the source again and installs only when it still answers those bytes; otherwise the request becomes `superseded`, nothing is installed, and the requester can ask again.
One pending request per source: asking twice answers the first request. A pending request expires after the TTL (14 days by default, FILEX_PLUGIN_REQUEST_TTL_DAYS).
Every step writes an audit row: created, approved, rejected, expired, superseded.
Index ¶
- Constants
- type Actor
- type CreateInput
- type Error
- type Options
- type Service
- func (s *Service) Approve(ctx context.Context, id int64, who Actor, lang string) (*model.PluginRequest, any, error)
- func (s *Service) ApproveWith(ctx context.Context, id int64, who Actor, lang string, detail map[string]any) (*model.PluginRequest, any, error)
- func (s *Service) Create(ctx context.Context, in CreateInput, who Actor) (*model.PluginRequest, bool, error)
- func (s *Service) ExpireDue(ctx context.Context) int
- func (s *Service) Get(ctx context.Context, id int64) (*model.PluginRequest, error)
- func (s *Service) List(ctx context.Context, status string) ([]*model.PluginRequest, error)
- func (s *Service) Reject(ctx context.Context, id int64, who Actor, note string) (*model.PluginRequest, error)
- func (s *Service) StartSweeper(ctx context.Context)
- func (s *Service) TTL() time.Duration
- type Source
- type Superseded
Constants ¶
const ( SourceGitHub = "github" SourceURL = "url" SourceFeed = "source" SourceFromSource = "from_source" )
Source kinds (plugin_requests.source_kind).
const ( AuditActionPluginRequestCreate = "plugin_request.create" AuditActionPluginRequestApprove = "plugin_request.approve" AuditActionPluginRequestReject = "plugin_request.reject" AuditActionPluginRequestExpire = "plugin_request.expire" AuditActionPluginRequestSupersede = "plugin_request.supersede" )
The audit actions a request writes. Constants, so the admin panel's audit labels are held to them (web/tests/lib/auditLabel.test.ts reads them).
const DefaultTTL = 14 * 24 * time.Hour
DefaultTTL is how long a request waits for a decision.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type CreateInput ¶
type CreateInput struct {
Kind string `json:"kind"`
Op string `json:"op"`
// Name: a storage plugin's name (install), or the installed plugin an
// upgrade replaces (either kind; PluginID works too).
Name string `json:"name"`
PluginID int64 `json:"plugin_id"`
Reason string `json:"reason"`
Source
}
CreateInput is one request as the caller asks for it.
type Options ¶
type Options struct {
Store db.Store
// Apps is nil when app plugins are off; Plugins nil when storage
// plugins are off. A request of that kind is then refused (503).
Apps *wasmplugin.Registry
Plugins *plugin.Manager
// Notify tells the administrators a request is waiting (nil: nobody).
Notify notify.Service
TTL time.Duration
Now func() time.Time
Log *slog.Logger
}
Options wire a Service.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service keeps the requests.
func (*Service) Approve ¶
func (s *Service) Approve(ctx context.Context, id int64, who Actor, lang string) (*model.PluginRequest, any, error)
Approve installs what the request froze, and only that. It answers the closed request and the installed plugin's status; *Superseded when the source changed (the request is closed as superseded); any other failure leaves the request pending, with the refusal kept on it.
func (*Service) ApproveWith ¶ added in v0.50.0
func (s *Service) ApproveWith(ctx context.Context, id int64, who Actor, lang string, detail map[string]any) (*model.PluginRequest, any, error)
ApproveWith is Approve with what else the administrator decided on the way (0.50: the File types choices), written into the approval's audit row.
func (*Service) Create ¶
func (s *Service) Create(ctx context.Context, in CreateInput, who Actor) (*model.PluginRequest, bool, error)
Create resolves a request's source (the dry run), freezes what it answered and records the request — or, when one is already pending for the same source, answers that one (created=false) and records nothing.
func (*Service) ExpireDue ¶
ExpireDue closes every pending request past its expiry and answers how many. It runs before every read and write here, and hourly (StartSweeper), so a request is never shown as waiting after its time.
func (*Service) Reject ¶
func (s *Service) Reject(ctx context.Context, id int64, who Actor, note string) (*model.PluginRequest, error)
Reject closes a request without installing anything; note is optional.
func (*Service) StartSweeper ¶
StartSweeper expires overdue requests every hour until ctx ends.
type Source ¶
type Source struct {
// An app from a GitHub repository (+ ref; default main, then master).
GitHubRepo string `json:"github_repo,omitempty"`
Ref string `json:"ref,omitempty"`
// An app from explicit addresses (the module's, the manifest's), or a
// storage plugin's binary by address. SHA256 pins the bytes.
URL string `json:"url,omitempty"`
ManifestURL string `json:"manifest_url,omitempty"`
SHA256 string `json:"sha256,omitempty"`
Signature string `json:"signature,omitempty"`
// A storage plugin's source: `owner/name` or a filex-storage.json address.
Source string `json:"source,omitempty"`
// An upgrade from the installed plugin's own source (the default for an
// upgrade that names no other).
FromSource bool `json:"from_source,omitempty"`
}
Source is where a request's plugin comes from — the install endpoints' own shapes, flattened into the request body.
type Superseded ¶
type Superseded struct{ Why string }
Superseded is Approve's answer when the source no longer serves the bytes the request froze: the request is closed as superseded, nothing installed.
func (*Superseded) Error ¶
func (e *Superseded) Error() string