Plugins
A plugin answers one yes/no question about one file, fixed in its signed manifest. The only thing that leaves it is 1 or 0.
Questions are about format, structure, integrity or rendering, never about what the content means or says. Plugins take no parameters: with one bit of output, a parameter is the way to aim that bit at the content (“does page 2 contain X?”). A variant is another plugin, with its own question and its own review.
The official marketplace
The lockfile-marketplace repository holds the official plugins, one Go module each, with the SDK and the conformance harness they are tested with. Its signer is pinned in the lockfile binary. Its source holds these 13 plugins, all at version 1.0.0:
| Plugin | Question | 1 means |
|---|---|---|
file.non-empty | Does the file contain at least one byte? | size > 0 |
format.is-pdf | Does the file start like a PDF? | %PDF- appears within the first 1024 bytes |
pdf.parses | Does the file parse as a PDF without errors? | pdfcpu reads and validates it (relaxed mode) |
pdf.truncated | Does the PDF look truncated? | no %%EOF in the last 1024 bytes, or the xref table needs repair |
pdf.encrypted | Is the PDF encrypted? | an /Encrypt dictionary is present |
pdf.has-blank-page | Does the PDF have at least one page that paints nothing visible? | at least one page has no painting operator, or only full-page white fills |
pdf.all-pages-blank | Does every page of the PDF paint nothing visible? | no page has a painting operator other than full-page white fills |
pdf.fonts-not-embedded | Does the PDF use at least one font that is not embedded? | a used font has no FontFile, FontFile2 or FontFile3 |
pdf.has-javascript | Does the PDF contain JavaScript? | a JavaScript action, a /JS entry or a JavaScript name tree |
pdf.has-incremental-update | Was the PDF modified by at least one incremental update after its first save? | the cross-reference chain has more than one section |
pdf.has-non-a4-page | Does the PDF have at least one page whose visible size is not A4? | a page’s crop box differs from 210 × 297 mm by more than 1 mm on a side |
pdf.has-rotated-page | Does the PDF have at least one page with a /Rotate other than 0? | a page’s effective /Rotate is 90, 180 or 270 |
pdf.claims-pdfa | Does the PDF declare PDF/A conformance in its XMP metadata? | the catalog’s XMP has a pdfaid:part property; conformance itself is not checked |
The exact wording of each question and of both answers is in the plugin’s plugin.toml, and lockfile plugins shows it to the agent. A plugin becomes installable once its tag is released and listed in the marketplace’s signed index.
Installing and updating (console account)
lockfile plugin install pdf.parses@1.0.0 # exact versions only; signatures checked with cosign
lockfile plugin update pdf.parses@1.1.0 # shows the manifest diff; a changed question needs the name typed
lockfile plugin list
lockfile plugin verify [NAME]
lockfile plugin remove NAME
An install checks the signed index, then the signed manifest and binary and their hashes. A running console treats a newly installed plugin as a policy loosening and applies it only after :review.
Unofficial marketplaces
lockfile marketplace add acme --index https://plugins.acme.example/index.json \
--identity-regexp '^https://github\.com/acme/lockfile-plugins/\.github/workflows/release\.yml@refs/tags/'
lockfile marketplace list
lockfile marketplace remove acme
An unofficial marketplace publishes the same index format, signed keyless by its own CI identity. It needs its name typed when added, is flagged in red on every approval screen and in every agent result (warning: unofficial_marketplace), and never runs in auto mode. Keyless signers only in v0.2: --public-key is refused.
Write a plugin
The plugin contract is the reference. In short:
The manifest
name = "pdf.has-blank-page"
version = "1.0.0"
protocol = 1
question = "Does the PDF have at least one page that paints nothing visible?"
one = "at least one page has no painting operator, or only full-page white fills"
zero = "every page paints something"
accepts = ["application/pdf"]
limits = { cpu_s = 20, memory = "512MiB", timeout_s = 30 }
acceptsdocuments the inputs; the console does not sniff the type, so the plugin checks its own input.- Changing
question,oneorzeroneeds a new major version, and users type the plugin name to accept it.
The runtime
| What | Value |
|---|---|
| Input | the file as an inherited read-only fd 3; no path |
| stdin | /dev/null |
| Environment | empty except LOCKFILE_PLUGIN_PROTOCOL=1; cwd / |
| Filesystem | Landlock: execute and read its own binary only |
| Network | none (seccomp denies sockets; Landlock denies TCP with ABI ≥ 4) |
| Limits | CPU seconds, address space, no core, file size 0, few processes, wall-clock timeout then SIGKILL of the group |
| stderr | drained and discarded; only its byte count is audited |
The answer
Write exactly 1 or 0 on stdout (one trailing newline tolerated) and exit with status 0. Anything else is a failure: other or more bytes, nothing, a non-zero exit, a signal, the timeout, an rlimit. Every failure reaches the agent as plugin_error with result: null, which is not a 0; the precise cause goes to the audit only. Exit-code-only answers are not accepted: a panic must never read as an answer.
The marketplace’s sdk/ module gives the fd-3 reader, the answer helpers and a test harness that runs a plugin under the same sandbox on synthetic fixtures.
Publishing
Official plugins go through the marketplace pipeline: a pull request with synthetic fixtures for both answers (no real document is ever committed), CI (vet, staticcheck, govulncheck, contract tests under the sandbox, fuzzing, an import check), a maintainer’s review against REVIEW.md, then a tag <name>/vX.Y.Z whose release workflow builds, hashes and signs the binaries and the manifest with cosign keyless, and a signed index.json.