Repository navigation
Extending Writing Lua Plugins
Prev: Choosing an Extension Model | Up: Extending ReqPack | Next: Building Registry Entries
Lua plugins are the main extension point for ReqPack.
They implement package-manager behavior while ReqPack provides config, planning, audit integration, output handling, and remote execution.
This page describes normal wrapper or proxy plugins loaded through LuaBridge, not hook scripts inside native .rqp packages.
Local plugin bundle layout:
plugins/
<plugin-id>/
metadata.json
reqpack.lua
run.lua
scripts/
install.lua
remove.lua
Important:
- plugin implementation must expose global
plugintable, -
metadata.json.nameis plugin id used for discovery, - planner and executor dependencies belong in
reqpack.luadepends, -
scripts/install.luaandscripts/remove.luamust exist even if they only returntrue.
Examples in this repo:
plugins/dnf/run.luaplugins/maven/run.luaplugins/java/run.luaplugins/sys/run.lua
ReqPack validates the Lua plugin contract at load time. These methods are required:
function plugin.getName() end
function plugin.getVersion() end
function plugin.getRequirements() end
function plugin.getCategories() end
function plugin.getMissingPackages(packages) end
function plugin.install(context, packages) end
function plugin.installLocal(context, path) end
function plugin.remove(context, packages) end
function plugin.update(context, packages) end
function plugin.list(context) end
function plugin.search(context, prompt) end
function plugin.info(context, packageName) endOptional methods:
function plugin.init() end
function plugin.shutdown() end
function plugin.outdated(context) end
function plugin.resolvePackage(context, package) end
function plugin.resolveProxyRequest(context, request) end
function plugin.getSecurityMetadata() endOptional static data:
plugin.fileExtensions = { ".rpm" }plugin.fileExtensions is metadata table, not function.
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)
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 {}
endReqPack exposes a rich plugin context.
context.plugin.id
context.plugin.dir
context.plugin.scriptcontext.flags
context.host
context.proxy
context.repositoriescontext.log.debug("...")
context.log.info("...")
context.log.warn("...")
context.log.error("...")context.tx.status(42)
context.tx.progress(50)
context.tx.begin_step("resolve dependencies")
context.tx.commit()
context.tx.success()
context.tx.failed("something went wrong")context.tx.progress(...) can also accept a richer payload with byte counters and rates.
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)These events feed ReqPack's display and structured output layers.
local result = context.exec.run("dnf repoquery curl")
local tmpDir = context.fs.get_tmp_dir()
local ok = context.net.download(url, destination)
context.artifacts.register({ type = "file", path = "/tmp/out" })Important runtime boundaries:
-
context.fsin Lua plugins currently exposes temp-directory helper, not general copy/mkdir helpers. -
context.net.download(...)returns success boolean. -
context.artifacts.register(...)records artifact payload for ReqPack; shape is plugin-defined JSON-like table. - if you need richer filesystem helpers for install/remove hooks, that is native
rqphook runtime, not normal Lua plugin runtime.
There is also a global namespace:
local result = reqpack.exec.run("command -v dnf >/dev/null 2>&1")
local host = reqpack.hostUse context.exec.run(...) inside action methods when possible, because it carries the current item identity and display correlation.
Relevant fields:
actionsystemnameversionsourcePathlocalTargetflagsdirectRequest
actionsystempackagesflagsoutputFormatoutputPathlocalPathusesLocalTarget
Useful fields include:
-
name,packageId,version,latestVersion -
status,installed -
summary,description -
homepage,documentation,sourceUrl,repository -
channel,section,packageType,architecture,license -
dependencies,optionalDependencies,provides,conflicts,replaces,binaries,tags -
extraFieldsfor additional key/value output
You do not need to fill every field. Fill the ones your ecosystem can provide reliably.
ReqPack uses getMissingPackages() during planning to avoid redundant work.
Good plugin behavior:
- return only packages that still need action,
- for installs: packages not already present,
- for removes: packages that are actually present,
- for updates: packages with available newer versions.
If this function is lazy and always returns everything, ReqPack still works, but planning quality and UX get worse.
If your plugin can resolve a package into an exact version, implement resolvePackage().
This helps with:
- vulnerability matching,
- SBOM exports,
- explicit package identity,
- better audit decisions when users do not specify exact versions.
Without it, unresolved-version policy may block installs or reduce audit fidelity.
When you add a new ecosystem, this is the practical minimum validation path:
- implement
resolvePackage()if exact version lookup is possible, - return
osvEcosystemfromgetSecurityMetadata()or configuresecurity.osvEcosystemMap, - run audit with a local feed path,
- run SBOM export for explicit packages 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 jsonIf audit fails with ecosystem-mapping or unresolved-version errors, fix those before calling the plugin production-ready.
ReqPack now ships a hermetic plugin test command for wrapper/plugin authors.
Use it when you want to validate plugin behavior without talking to a real package manager:
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. That keeps runtime small because ReqPack already embeds Lua.
Minimal case:
return {
name = "install success",
request = {
action = "install",
system = "demo",
packages = {
{ name = "curl", version = "8.0" }
}
},
fakeExec = {
{
match = "demo-pm install curl",
exitCode = 0,
stdout = "ok\n",
stderr = "",
success = true,
}
},
expect = {
success = true,
commands = { "demo-pm install curl" },
stdout = { "ok\n" },
events = { "installed", "success" },
eventPayloads = {
installed = "{name=curl}",
success = "ok",
},
}
}What ReqPack checks today:
- command strings executed through
context.exec.run(...)orreqpack.exec.run(...) - boolean success/failure
- fake stdout and stderr returned by command layer
- emitted event names
- event payload text
- registered artifacts
- first query result count/name/version
Practical advice:
- Use explicit
matchstrings that uniquely identify one command. - Keep fake stdout deterministic and easy to parse.
- Add at least one failure case, not only happy paths.
- Put reusable baseline cases in
.reqpack-test/core/so--preset coreworks. - Still run a few real smoke tests later for target OS/package manager.
Proxy plugins should not implement real package-manager behavior directly. They should transform the request and hand it to a real target system.
Minimal shape:
function plugin.resolveProxyRequest(context, request)
return {
targetSystem = "maven",
flags = request.flags,
packages = request.packages,
}
endRules:
- do not return both
packagesandlocalPath - do not resolve to yourself
- stay within configured
context.proxy.targetswhen present - if you return
flags, they replace request flags for next resolved request
If you want better trust and security behavior, implement getSecurityMetadata().
Example:
function plugin.getSecurityMetadata()
return {
role = "package-manager",
capabilities = { "exec", "network" },
ecosystemScopes = { "rpm" },
writeScopes = {
{ kind = "temp" },
{ 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,
}
endThis becomes especially important when security.requireThinLayer = true.
- Keep the plugin thin. Let the real package manager do real package-manager work.
- Emit events and transaction steps. They improve UX, remote output, and logs.
- Implement
installLocal()if your ecosystem supports local artifacts. - Implement
resolvePackage()as soon as exact version resolution is possible. - Return realistic
PackageInforecords.infoandoutdatedshould not be placeholders in mature plugins. - Prefer deterministic shell commands and parse outputs conservatively.
- Use
plugin.fileExtensionsif local-file inference makes sense. - Test plugin through actual
rqpcommands because runtime contract is behavioral, not only structural.
Prev: Choosing an Extension Model | Up: Extending ReqPack | Next: Building Registry Entries
- 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