-
Notifications
You must be signed in to change notification settings - Fork 0
Extending Writing Lua Plugins
Prev: Choosing an Extension Model | Up: Extending ReqPack | Next: Lua Plugin Cookbook
Lua plugins are main ReqPack extension point. They wrap package managers and proxy systems while ReqPack handles discovery, planning, request resolution, display, transactions, history, security, audit, and SBOM flows.
This page describes normal Lua plugins loaded through LuaBridge.
It does not describe native .rqp hook scripts.
ReqPack recognizes plugin bundle only if all of these files exist:
plugins/
<plugin-id>/
metadata.json
reqpack.lua
run.lua
scripts/
install.lua
remove.lua
Bundle loader requirements:
-
metadata.jsonmust be valid JSON with non-emptyformatVersion = 1,name,version,summary,description, andlicense. -
reqpack.luamust return manifest table withapiVersion = 1. -
reqpack.lua.dependsis optional array of dependency specs. -
run.luais actual Lua plugin implementation. -
scripts/install.luaandscripts/remove.luaare required for bundle validity even though normalLuaBridgeplugins do not execute them.
Minimal reqpack.lua:
return {
apiVersion = 1,
depends = {
"curl",
"dnf:python3@3.12",
}
}Dependency spec forms accepted by bundle manifest:
packagepackage@versionsystem:packagesystem:package@version
Important versioning detail:
- bundle manifest version is
reqpack.lua.apiVersion = 1 - runtime plugin interface version is C++
REQPACK_API_VERSION = 5
Do not mix them.
metadata.json.name becomes plugin id used by runtime and is normalized to lowercase.
That id is what you see in context.plugin.id.
Examples in this repo:
plugins/dnf/run.luaplugins/maven/run.luaplugins/java/run.luaplugins/sys/run.lua
LuaBridge opens these standard Lua libraries:
basetablestringmathio
Not opened by default:
ospackagedebugcoroutine
Extra globals injected by ReqPack:
REQPACK_PLUGIN_IDREQPACK_PLUGIN_DIRREQPACK_PLUGIN_SCRIPTreqpack
print(...) is overridden and routed into ReqPack output.
Arguments are tab-joined.
reqpack currently exposes:
reqpack.exec.run(command)
reqpack.hostPrefer context.exec.run(...) inside plugin action methods.
It keeps output correlated to current item when ReqPack has single-item context.
Plugin must expose global plugin table.
Required methods:
| Method | Return | Notes |
|---|---|---|
plugin.getName() |
string |
Read during bridge construction, before init()
|
plugin.getVersion() |
string |
Read during bridge construction, before init()
|
plugin.getRequirements() |
Package[] |
Required by interface; keep aligned with bundle manifest |
plugin.getCategories() |
string[] |
Can be called on constructed plugin before init()
|
plugin.getMissingPackages(packages) |
Package[] |
Source of truth for desired-state filtering |
plugin.install(context, packages) |
bool or no return |
Batch install/ensure |
plugin.installLocal(context, path) |
bool or no return |
Local file/archive install |
plugin.remove(context, packages) |
bool or no return |
Batch remove |
plugin.update(context, packages) |
bool or no return |
Batch update |
plugin.list(context) |
PackageInfo[] |
Query |
plugin.search(context, prompt) |
PackageInfo[] |
Query |
plugin.info(context, packageName) |
PackageInfo |
Query |
Optional methods:
| Method | Return | Notes |
|---|---|---|
plugin.init() |
bool or no return |
Called once on first successful loadPlugin()
|
plugin.shutdown() |
bool or no return |
Called on registry shutdown when enabled |
plugin.outdated(context) |
PackageInfo[] |
Used by rqp outdated
|
plugin.resolvePackage(context, package) |
Package or nil
|
Used by SBOM/audit resolution |
plugin.resolveProxyRequest(context, request) |
ProxyResolution or nil
|
Used during proxy expansion |
plugin.getSecurityMetadata() |
table or nil
|
Read during bridge construction, before init()
|
plugin.pack(context, projectPath, outputPath, flags) |
bool or no return |
Optional rqp pack <system> ... support |
Optional static data:
plugin.fileExtensions = { ".rpm", ".deb" }plugin.fileExtensions is table, not function.
It is read during bridge construction, before init().
Boolean-return methods treat no return as success. Query methods treat no return as empty result.
Actual call flow is:
- Registry scans bundle directories and validates bundle layout from
metadata.jsonandreqpack.lua. - First
getPlugin()orloadPlugin()constructsLuaBridge. - Bridge construction executes
run.luaimmediately. - During construction ReqPack eagerly reads:
plugin.getName(),plugin.getVersion(),plugin.getSecurityMetadata(), andplugin.fileExtensions. - First successful
loadPlugin()validates required method contract, checks interface version, then calls optionalplugin.init(). - Planner, request resolution, queries, and executor then call plugin methods as needed.
- Optional
plugin.shutdown()runs when registry shuts plugins down. - Temp directories created by
context.fs.get_tmp_dir()are deleted during bridge shutdown.
Important behavior:
-
run.luaside effects happen beforeinit(). -
getCategories()can be called on constructed plugin beforeinit()because registry category lookup does not force load. -
plugin.fileExtensionsis available beforeinit()and is used for local-target system detection. - Query and action methods normally run after
init()because executor/request-resolution paths callloadPlugin()first.
ReqPack calls plugin methods like this:
| ReqPack path | Plugin method |
|---|---|
| proxy expansion | resolveProxyRequest(context, request) |
| install / ensure with package names | install(context, packages) |
| install with local file/path | installLocal(context, path) |
| remove | remove(context, packages) |
| update with package list | update(context, packages) |
| update whole system | update(context, {}) |
| list | list(context) |
| outdated | outdated(context) |
| search | search(context, prompt) |
| info | info(context, packageName) |
| sbom/audit exact resolution | resolvePackage(context, package) |
| plugin-specific pack | pack(context, projectPath, outputPath, flags) |
More exact semantics:
-
search()receives one string prompt built by joining all request package arguments with spaces. -
info()receives only first requested package name. -
list(),search(), andoutdated()results are post-filtered by ReqPack forarch=<value>andtype=<value>flags. - Executor batches packages by
(action, system), soinstall(),remove(), andupdate()may receive many packages at once. -
installLocal()receives resolved local path. Upstream CLI/orchestrator may already have downloaded URL or extracted archive before your plugin sees it.
Item correlation behavior:
- if batch contains exactly one package, runtime events and exec output are tied to
<system>:<package> - if call uses local target, they are tied to
<system>:local - if call is pack, they are tied to
<system> - multi-package batches fall back to plugin scope, not individual package rows
Two dependency surfaces exist.
reqpack.lua.depends:
- bundle-level dependency manifest
- used by planner for plugin dependency provisioning
- should be source of truth for shipped plugin bundle requirements
plugin.getRequirements():
- still required by runtime interface
- used by some execution/history fallback paths when bundle manifest is not available
Best practice: keep both consistent.
plugin = {}
function plugin.getName()
return "Example Manager"
end
function plugin.getVersion()
return "1.0.0"
end
function plugin.getRequirements()
return {}
end
function plugin.getCategories()
return { "example" }
end
function plugin.getMissingPackages(packages)
return packages
end
function plugin.install(context, packages)
context.tx.begin_step("install example packages")
local result = context.exec.run("example-pm install ...")
if not result.success then
context.tx.failed("example install failed")
return false
end
context.events.installed(packages[1].name)
context.tx.success()
return true
end
function plugin.installLocal(context, path)
return false
end
function plugin.remove(context, packages)
return true
end
function plugin.update(context, packages)
return true
end
function plugin.list(context)
return {}
end
function plugin.search(context, prompt)
return {}
end
function plugin.info(context, packageName)
return { name = packageName, version = "1.0.0" }
endRaw request flags for current call. These may include normal CLI flags and internal runtime markers. Plugin should ignore unknown flags it does not understand.
context.plugin.id
context.plugin.dir
context.plugin.scriptThese map to:
- plugin id from bundle metadata
- plugin bundle directory
- absolute path to
run.lua
Ordered list of repositories for current ecosystem only.
ReqPack sorts them by ascending priority before exposing them.
Each repository entry contains:
repo.id
repo.url
repo.priority
repo.enabled
repo.type
repo.auth.type
repo.auth.username
repo.auth.password
repo.auth.token
repo.auth.sshKey
repo.auth.headerName
repo.validation.checksum
repo.validation.tlsVerify
repo.scope.include
repo.scope.excludeAny repository extras fields from config are copied as top-level keys on repository entry.
Example: repo.snapshots, repo.tags.
Either nil or table:
context.proxy.default
context.proxy.targets
context.proxy.optionsdefault is default target system.
targets is allowed target list from config.
options is string map from proxy config.
Per-call host snapshot.
Optional values may be nil.
This is ReqPack host-introspection API for plugin authors. Use it when plugin needs OS family, distro info, CPU model, logical/physical core count, memory, GPU, or mounted-storage details.
Example:
local os_name = context.host.os.name
local cpu_model = context.host.cpu.model
local logical = context.host.cpu.logicalCores
local physical = context.host.cpu.physicalCorescontext.host.platform.osFamily
context.host.platform.arch
context.host.platform.target
context.host.platform.supportLevel
context.host.platform.supportReason
context.host.os.family
context.host.os.id
context.host.os.name
context.host.os.version
context.host.os.versionId
context.host.os.prettyName
context.host.os.distroId
context.host.os.distroName
context.host.kernel.name
context.host.kernel.release
context.host.kernel.version
context.host.cpu.arch
context.host.cpu.vendor
context.host.cpu.model
context.host.cpu.logicalCores
context.host.cpu.physicalCores
context.host.memory.totalBytes
context.host.memory.availableBytes
context.host.gpus[i].vendor
context.host.gpus[i].model
context.host.gpus[i].driverVersion
context.host.gpus[i].backend
context.host.storage.mounts[i].device
context.host.storage.mounts[i].mountPoint
context.host.storage.mounts[i].fsType
context.host.storage.mounts[i].totalBytes
context.host.storage.mounts[i].usedBytes
context.host.storage.mounts[i].availableBytes
context.host.storage.mounts[i].readOnly
context.host.cache.schemaVersion
context.host.cache.collectedAtEpoch
context.host.cache.expiresAtEpoch
context.host.cache.refreshReason
context.host.cache.sourcereqpack.host exposes same host shape, but it is bridge-global snapshot created during plugin construction.
Prefer context.host for request-time data.
If snapshot looks stale, run rqp host refresh.
context.log.debug("...")
context.log.info("...")
context.log.warn("...")
context.log.error("...")These emit plugin-scoped log messages unless silent runtime mode is active.
context.tx.status(42)
context.tx.progress(50)
context.tx.progress({
percent = 25,
current = 10.0,
currentUnit = "MiB",
total = 40.0,
totalUnit = "MiB",
speed = 2.5,
speedUnit = "MiB/s",
})
context.tx.begin_step("resolve dependencies")
context.tx.commit()
context.tx.success()
context.tx.failed("something went wrong")Progress payload rules:
-
percentis clamped to0..100 -
current,total, andspeedmust be numeric - units accepted by progress normalization are
B,KB,KiB,MB,MiB,GB,GiB,TB,TiB, and/svariants - if
percentis missing butcurrentandtotalexist, ReqPack derives percent - if
currentis missing buttotalandpercentexist, ReqPack derives current bytes
Available event helpers:
context.events.installed(payload)
context.events.deleted(payload)
context.events.updated(payload)
context.events.listed(payload)
context.events.searched(payload)
context.events.informed(payload)
context.events.outdated(payload)
context.events.unavailable(payload)Payload serialization rules:
- strings, booleans, and numbers become plain text
- tables become sorted text like
{key=value, other=value} - unsupported non-table Lua values serialize as
<lua-value>
These events feed display and structured output.
Special unavailable behavior:
- executor uses recent
unavailableevents during transactional failure handling - payload should match current package request spec exactly
- current executor matches
nameorname-version
Registers artifact payload for ReqPack. Payload is serialized with same rules as event payloads.
For plugin.pack(), register artifact path strings.
rqp pack uses plugin.takeRecentArtifacts() after call.
Overloads:
local result = context.exec.run("command")
local result = context.exec.run("command", rules)Return type:
result.success
result.exitCode
result.stdout
result.stderrActual runner behavior:
- shell command runs as
/bin/sh -c <command> - stdout and stderr are merged into same transcript
- merged transcript is always appended to
result.stdout - on success,
result.stderris normally empty even if command wrote to stderr - on non-zero exit, if no runner-level read error happened, ReqPack copies merged transcript into
result.stderr
If you need output parsing side effects, pass exec rules as second argument.
Creates temp directory and returns its path. ReqPack deletes these temp directories during plugin shutdown.
Downloads resource to destination path and returns true or false.
Behavior details:
- Lua API gets boolean only, not
DownloadResult - if source looks like archive, ReqPack may extract it in place
- destination path may become directory after extraction
- local
file://sources also work
local result = reqpack.exec.run("command -v dnf >/dev/null 2>&1")
local host = reqpack.hostDifferences from context.exec.run(...):
- only command-string overload exists
- output is tied to plugin scope, not current item id
- same exec policy still applies
Fields visible in Lua:
package.action
package.system
package.name
package.version
package.sourcePath
package.localTarget
package.flagsNotes:
- incoming
Packageuserdata uses integeraction - when ReqPack parses plain Lua tables back into
Package, it accepts integer action values or stringsinstall,remove,update,search,list,info -
directRequestexists in C++ but is not exposed to Lua
Fields visible in Lua:
request.action
request.system
request.packages
request.flags
request.outputFormat
request.outputPath
request.localPath
request.usesLocalTargetpayloadPath exists in C++ request type but is not exposed to Lua.
ReqPack accepts either PackageInfo userdata or plain Lua tables.
Supported table fields are:
system
name
packageId
version
latestVersion
status
installed
summary
description
homepage
documentation
sourceUrl
repository
channel
section
packageType
type
architecture
targetSystems
license
author
maintainer
email
publishedAt
updatedAt
size
installedSize
dependencies
optionalDependencies
provides
conflicts
replaces
binaries
tags
extraFieldsNotes:
-
typeis alias forpackageType -
installedis string field, not boolean field - if
summaryis empty, ReqPack copiesdescriptioninto it - array fields must be string arrays
-
extraFieldscan be either map-like table:
extraFields = {
homepage = "https://example.test",
checksum = "sha256:...",
}or array of { key = ..., value = ... } tables:
extraFields = {
{ key = "checksum", value = "sha256:..." },
}{
success = true or false,
exitCode = 0,
stdout = "merged command output",
stderr = "policy error, runner error, or merged output on failure"
}resolveProxyRequest() must return table like:
{
targetSystem = "maven",
packages = { "org.junit:junit:4.13" },
flags = { "arch=noarch" },
}Supported keys:
-
targetSystemrequired string -
packagesoptional string array -
localPathoptional string -
flagsoptional string array
packages and localPath are mutually exclusive.
If plugin defines resolveProxyRequest(), returning nil is treated as resolution failure, not pass-through.
ReqPack uses getMissingPackages() in several places:
- install/ensure request filtering
- executor pre-dispatch filtering for install/ensure
- transaction recovery reconciliation
- per-package partial success/failure classification after failed install/remove/update/ensure call
Return only packages that still need desired end state.
Practical meaning:
- install / ensure: packages not yet installed
- remove: packages still present and still need removal
- update: packages still not at desired version
If this function is lazy and always returns input, ReqPack still works, but planning quality, recovery, and failure classification get worse.
resolvePackage(context, package) is optional but strongly recommended if your ecosystem can resolve exact version.
ReqPack uses it for:
- SBOM export
- security/audit matching
- exact package identity when request omitted version
If plugin does not implement it, executor falls back to:
- existing version on incoming package, or
-
info()version lookup when possible
That fallback is weaker than real package resolution.
Proxy resolution is recursive and code-enforced.
Current rules:
- max depth is
4 - cycle is rejected
- resolving to self is rejected
- when proxy config has explicit
targets, resolved target must be in that list - if plugin implements
resolveProxyRequest(), returningnilfails request resolution - returned
flagsreplace current request flags - returned
packagesreplace package list and clear local target - returned
localPathsetsusesLocalTarget = trueand clears package list
Minimal example:
function plugin.resolveProxyRequest(context, request)
local target = context.proxy.default or context.proxy.targets[1]
return {
targetSystem = target,
packages = request.packages,
flags = request.flags,
}
endExec rules let plugin parse command output and emit ReqPack events while command is running.
Top-level schema:
{
initial = "default",
rules = {
{
state = "default",
source = "line",
regex = "^Loaded (.+)$",
repeat = true,
stop = false,
actions = {
{ type = "log", level = "info", message = "${1}" },
},
},
},
}Parser rules:
-
rulesmust be contiguous 1-based array-style table - top-level keys allowed only:
initial,rules - each rule must contain
source,regex,actions -
sourcemust belineorscreen -
regexuses C++std::regexECMAScript syntax -
actionsmust be non-empty contiguous 1-based array-style table - unknown keys are errors
- nested tables are not allowed inside action field values
Rule fields:
| Field | Type | Default | Notes |
|---|---|---|---|
state |
string | any state | Rule runs only when current state matches |
source |
"line" or "screen"
|
required |
screen implies PTY runner |
regex |
string | required | Compiled as ECMAScript regex |
actions |
action array | required | Executed in order on match |
repeat |
boolean | true |
false disables rule after first match |
stop |
boolean | false |
Stops further rule evaluation for current pass only |
Supported action types:
type |
Required fields | Notes |
|---|---|---|
send |
value |
Writes exact bytes to PTY child stdin |
state |
value |
Updates runtime state, emits nothing |
log |
message |
Optional `level = debug |
status |
code |
Integer status code |
progress |
one of percent, current, total, speed
|
Use currentUnit, totalUnit, speedUnit for byte normalization |
begin_step |
label |
Emits begin-step event |
success |
none | Emits success event with payload ok
|
failed |
message |
Emits failed event |
event |
name |
Optional payload
|
artifact |
payload |
Emits artifact payload |
Placeholder substitution:
-
${0}is full regex match -
${1},${2}, ... are capture groups - substitution happens in all action fields
- invalid placeholder token is left unchanged
- numeric placeholder with no matching capture resolves to empty string
Runner selection:
- empty ruleset: plain shell runner
- rules but no
screenrule and nosendaction: line runner - any
screenrule or anysendaction: PTY runner
Line vs screen matching:
-
linerules run against each complete line, plus final unterminated line at EOF - line runner sees raw merged stdout/stderr lines
- PTY runner normalizes transcript first, then applies both line and screen rules
-
screenrules search normalized cumulative PTY transcript - each screen rule tracks its own cursor, so next match starts after previous match for that rule
PTY transcript normalization currently does all of this:
- strips ANSI CSI and OSC escape sequences
- turns
\rinto newline - applies backspace removal
- removes other control characters except newline and tab
Warnings and failures:
- malformed rules fail command immediately with
success = false,exitCode = 1 - invalid resolved action fields produce plugin warning logs and action is skipped
-
stop = truedoes not stop child process; it only stops checking later rules for that current evaluation pass
plugin.getSecurityMetadata() may return:
function plugin.getSecurityMetadata()
return {
role = "package-manager",
capabilities = { "exec", "network" },
ecosystemScopes = { "rpm" },
writeScopes = {
{ kind = "temp" },
{ kind = "plugin-data", value = "state" },
{ kind = "user-home-subpath", value = ".cache/dnf" },
},
networkScopes = {
{ host = "api.osv.dev", scheme = "https", pathPrefix = "/v1" },
},
privilegeLevel = "sudo",
osvEcosystem = "RPM",
purlType = "rpm",
versionComparatorProfile = "rpm-evr",
versionTokenPattern = "[A-Za-z0-9._+-]+",
versionCaseInsensitive = false,
}
endLoader normalization:
-
roleis lowercased -
capabilitiesare lowercased -
privilegeLevelis lowercased -
writeScopes[].kindis lowercased -
networkScopes[].hostandnetworkScopes[].schemeare lowercased
Shape rules:
- each
writeScopesentry must includekind - each
networkScopesentry must include at least one ofhost,scheme, orpathPrefix
Current write-scope kinds recognized by exec policy:
| kind | Meaning |
|---|---|
read-only |
never allows writes |
temp |
path under system temp directory |
plugin-data |
path under context.plugin.dir or its subpath |
reqpack-cache |
path under ReqPack cache directory |
user-home-subpath |
path under $HOME/<value>
|
system-package-paths |
path under /usr, /usr/local, /opt, /etc, /var/lib, or /var/cache
|
Thin-layer behavior today:
- when
security.requireThinLayer = true, registry can reject plugin if runtime metadata is weaker than registry record -
context.exec.run(...)andreqpack.exec.run(...)are denied withexitCode = 126if plugin lacksexeccapability - commands using
sudoordoasare denied unlessprivilegeLevel == "sudo" - command write targets are denied if they fall outside declared
writeScopes
Pack-specific allowance:
- during
plugin.pack(), ReqPack temporarily allows writes underprojectPath - it also allows writes under explicit output directory
- temp dirs created with
context.fs.get_tmp_dir()during pack are also allowed
Important limitation:
-
networkScopesare parsed and matched against trust records - current runtime exec policy does not yet use
networkScopesto blockcontext.net.download()
Minimum practical path:
- implement
resolvePackage()if exact version lookup is possible - return
osvEcosystemfromgetSecurityMetadata()or configuresecurity.osvEcosystemMap - run audit with local feed path
- run SBOM export for explicit and installed packages
Useful commands:
rqp audit <system> <package> --osv-feed ./test-data/osv --osv-refresh always
rqp audit <system> --strict-ecosystem-mapping
rqp sbom <system> --format jsonReqPack ships hermetic plugin test command for wrapper authors.
Examples:
rqp test-plugin --plugin ./plugins/demo --case ./cases/install.lua
rqp test-plugin --plugin demo --cases ./tests/plugins/demo
rqp test-plugin --plugin demo --preset core --report ./plugin-test-report.jsonSupported inputs:
-
--plugin <value>: plugin id, plugin bundle directory, or direct script path -
--case <file.lua>: add one Lua case file -
--cases <dir>: add all*.luafiles in directory -
--preset core: add preset cases from<plugin>/.reqpack-test/core/ -
--report <file.json>: write JSON summary report
Case files are Lua tables, not JSON.
ReqPack currently checks:
- commands executed through
context.exec.run(...)orreqpack.exec.run(...) - boolean success/failure
- captured stdout and stderr
- emitted event names and payloads
- registered artifacts
- first query result count, name, and version
- Keep wrapper thin. Let underlying package manager do package-manager work.
- Keep metadata methods side-effect free and safe before
init(). - Implement
getMissingPackages()carefully. It affects planning, recovery, and failure classification. - Prefer
context.exec.run(...)overreqpack.exec.run(...)inside action methods. - Emit
context.tx.*andcontext.events.*for better UX and structured output. - Implement
installLocal()if ecosystem supports local artifacts. - Implement
resolvePackage()as soon as exact version lookup is possible. - Return realistic
PackageInforecords. Avoid placeholderinfo()andoutdated()in mature plugins. - For
plugin.pack(), register produced artifact paths withcontext.artifacts.register(...). - Test through real
rqpcommands after hermetic tests.
- Lua Plugin Cookbook
- Building Registry Entries
- Testing Lua Plugins
- Security, Audit, and SBOM
- Architecture Overview
Prev: Choosing an Extension Model | Up: Extending ReqPack | Next: Lua Plugin Cookbook
- User Guide
- Getting Started
- Command Reference
- Configuration
- Configuration Reference
- Security, Audit, and SBOM
- Output and Report Formats
- Remote Mode
- Remote Protocol Reference
- Using Native
rqpPackages - Troubleshooting