Skip to content

Add the OPR packaging request schema and its validator - #292

Open
firemanxbr wants to merge 1 commit into
omacom:masterfrom
firemanxbr:opr-request-validator
Open

Add the OPR packaging request schema and its validator#292
firemanxbr wants to merge 1 commit into
omacom:masterfrom
firemanxbr:opr-request-validator

Conversation

@firemanxbr

Copy link
Copy Markdown

Adds the packaging request format and a validator for it. This is "Next work" item 1 from the OPR design: the check that runs on every request PR, before a human reads the request.

Scope is the validator only — no factory, no build pipeline, no PKGBUILD generation.

What's here

docs/request-schema.md the format spec
schema/request.schema.json JSON Schema, validates the parsed TOML, fails closed on unknown keys
requests/ghostty.toml Surface A example
requests/google-chrome.toml Surface B example
tools/opr-request-validate/ the validator: ~2,900 lines, 112 tests

The spec files and the validator are in one PR because the validator embeds schema/request.schema.json via include_str! and a test validates the committed requests/*.toml — it does not build without them.

Using it

opr-request-validate --catalogue catalogue.txt requests/ghostty.toml

Exit 0 valid, 1 violations, 2 could not run. One line per violation, naming the field and the rule:

requests/ghosttty.toml: rule 5: package.name: 'ghosttty' is one edit away from the
existing package 'ghostty', so it is held for typosquat review — if you meant
'ghostty', it already exists and needs no request. If this is a genuinely different
project, say so in requester.notes and a maintainer will clear it [...]

Every violation carries a remedy, asserted by a test. These are read by people filing a first request, for whom invalid upstream is a dead end.

Two stages

rules network
--offline (default) 1-5, 7-11 none
--resolve plus 6 resolves upstream

The offline stage is complete on its own and cannot reach the network. Enforced, not asserted:

  • an offline rule's check signature receives no HTTP handle, so the compiler rejects any attempt to add one;
  • jsonschema is built default-features = false — its defaults enable reqwest + resolve-http + resolve-file, which would link a remote $ref resolver into the offline binary.

Verified under a network-denied sandbox in both directions: offline passes, --resolve fails. That pair is the evidence, and the README has the command.

Design notes for review

Structural checks are not reimplemented in Rust. The JSON Schema is the single definition of the format; duplicating it would create two that drift. What is hand-written is the translation from a schema error into an actionable sentence.

The binary refuses to start if the schema stops failing closed. "additionalProperties": false is what stops the format growing by smuggling, it lives in one keyword per object, and a future edit could drop one silently. Startup walks the schema and aborts if any object schema has lost it.

Request values are untrusted on the way out too. Nothing here builds a shell command, so there is no interpolation site; what remains is display safety. Values reaching a message are escaped, length-capped and delimited. A test asserts no raw control, bidi or zero-width character reaches stdout even when the request is built to produce one.

Decisions needed before merge

The crate README documents nine places where the implementation had to interpret the spec. Three want your call:

  1. requests/google-chrome.toml fails rule 6 as literally written. Rule 6 requires upstream to be a repository root; the Surface B example points at https://dl.google.com/linux/chrome/deb, which is not one and cannot be. I exempted source.kind = "vendor" from the repository-root assertion, keeping HTTPS, resolution and the off-host check. The alternative reading is that the example is wrong and Surface B needs a different identity anchor. This is a design question, not an oversight.

  2. Rule 8's allowlist does not exist in the spec. The rule names "the redistributable allowlist"; nothing defines its contents. data/redistributable.txt is a conservative proposal, reviewable as its own diff. Absence means unreviewed, not refused — rejected with that reason, never silently downgraded to the recipe surface.

  3. Rule 4 needs the catalogue too, not just rule 5 — it checks collision against existing OPR and Arch core/extra names. One --catalogue serves both: distance 0 is rule 4, distance 1 is rule 5, so one mistake yields one diagnostic. If two lists were intended — everything that exists for rule 4, a top-N subset for rule 5 — that needs a second flag.

One gap in the rule as specified: rule 5 is Levenshtein, so transpositions pass. ghotsty is distance 2 from ghostty and clears the check despite being an obvious typosquat. Damerau-Levenshtein would catch it. Implemented as specified and pinned by a test so changing it is deliberate.

Also decided on fail-closed grounds: a missing --catalogue exits 2. --no-catalogue exists for local runs and must be passed deliberately. A gate that reports success because it was misconfigured is worse than one that fails.

Not included

  • The catalogue file itself (Arch core/extra + current OPR). CI must pass --catalogue; generating it is not part of this crate, and rules 4 and 5 are not live until someone does.
  • A PR gate step running the validator on changed requests/*.toml — it needs that catalogue first.
  • The musl static target is configured (rustls, not native-tls) but not yet built in CI.

Incidental

.gitignore's bare src/ rule, meant for pkgbuilds/*/src/ build directories, also matches a Cargo source directory and silently untracked the entire crate. Added a scoped negation. Worth knowing for any future tool in this repo.

The new request-validator CI job runs cargo fmt --check, cargo clippy -D warnings and cargo test. The suite needs no network: rule 6's redirect and host logic is tested against scripted responses through an Http trait, so it does not depend on what a third party serves today.

🤖 Generated with Claude Code

A packaging request is the only artifact in this system written by someone
outside the project, so it is the first and cheapest place a bad one should
stop. This adds the request format and a validator that runs on every PR.

docs/request-schema.md and schema/request.schema.json define the format.
requests/ghostty.toml and requests/google-chrome.toml are the Surface A and
Surface B examples.

tools/opr-request-validate implements the eleven rules in the spec's
Validation section, in two stages, because the PR check and the network
check are separate:

  --offline (default)  rules 1-5 and 7-11, no network at all
  --resolve            adds rule 6, which resolves the upstream URL

The offline stage is complete on its own and cannot reach the network.
That is enforced rather than asserted: an offline rule's check signature
receives no HTTP handle, so the compiler rejects any attempt to add one,
and jsonschema is built with default-features = false because its defaults
would link a remote $ref resolver into the binary. Both directions are
verified under a network-denied sandbox; see the crate README.

Structural checks are delegated to the JSON Schema rather than reimplemented
in Rust, so the format has a single definition. What is hand-written is the
translation from a schema error into a sentence a first-time submitter can
act on. The binary refuses to start if the schema it loads has stopped
saying "additionalProperties": false on any object, since failing closed on
unknown keys is the invariant that stops the format growing by smuggling.

Every request value is treated as untrusted on the way out as well as in.
Values reaching a message are escaped, length-capped and delimited, and a
test asserts that no raw control, bidirectional or zero-width character can
reach stdout even when the request is built to produce one.

The .gitignore change is needed because its bare `src/` rule, meant for
pkgbuilds/*/src/ build directories, also matches a Cargo source directory
and silently untracked the whole crate.

The crate README documents nine places where the implementation had to
interpret the spec. Three want a decision before this merges, the largest
being that requests/google-chrome.toml fails rule 6 as literally written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant