Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,24 @@ jobs:
./bin/omarchy-pkgs self-test
./bin/omarchy-release self-test
'

request-validator:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false

# The validator runs on every request PR, in a stage with no network and
# no secrets. `cargo test` covers that stage without one either: rule 6
# is the only networked rule and its tests drive scripted responses
# rather than the live internet.
- name: Build and test the request validator
working-directory: tools/opr-request-validate
run: |
set -euo pipefail
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ pkgbuilds/yay/yay/
.srcdest/
.repo-host
.worktrees/

# Rust tooling: the bare `src/` rule above is meant for pkgbuilds/*/src/
# build dirs, but it also matches a Cargo crate source directory.
!tools/*/src/
tools/*/target/
136 changes: 136 additions & 0 deletions docs/request-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Packaging request schema

**Status:** draft for review
**Schema version:** 1

## Purpose

A packaging request is the only artifact someone outside the project writes. Everything downstream — the PKGBUILD, the source list, the dependency set, the build flags — is generated by the factory and reviewed by a maintainer.

That is the whole point of the format. A request must be small enough for a human to read in fifteen seconds, structured enough for a machine to validate before a human ever sees it, and incapable of carrying executable content by construction.

## Format and location

TOML, one file per package, at `requests/<name>.toml`.

TOML over YAML because YAML's anchors, merge keys and implicit typing are parser attack surface we do not need. TOML over JSON because humans write these by hand.

One file per package rather than a manifest gives per-package `CODEOWNERS`, clean diffs, and a natural place for the verification record to live.

## Example

```toml
schema = 1

[package]
name = "ghostty"
description = "GPU-accelerated terminal emulator"
upstream = "https://github.com/ghostty-org/ghostty"
license = "MIT"
surface = "binary"
architectures = ["x86_64", "aarch64"]

[source]
kind = "release"

[requester]
relationship = "user"
notes = "Widely used in the Omarchy community; currently only available via AUR."
```

## Fields

### `schema`

Required. Integer. Currently `1`. A request with an unknown schema version is rejected rather than best-effort parsed.

### `[package]`

| Field | Required | Description |
|---|---|---|
| `name` | yes | pacman package name. Lowercase, `^[a-z0-9][a-z0-9@._+-]*$`, max 64 chars. |
| `description` | yes | One line for the catalog. Max 120 chars, no newlines. |
| `upstream` | yes | HTTPS URL of the **repository of record**. Not a homepage, not a download page, not a vendor landing page. |
| `license` | yes | SPDX identifier or expression, as declared by the requester. The factory verifies it against the repository. |
| `surface` | yes | `binary` or `recipe`. See below. |
| `architectures` | yes | Subset of `["x86_64", "aarch64"]`. |

**On `upstream`.** This field is the identity anchor for the whole package and it is the field most likely to be wrong in good faith. A vendor homepage is not a repository root, and accepting one means the factory has nothing to diff future versions against. Requests whose `upstream` does not resolve to a repository root are returned to the requester rather than guessed at.

**On `surface`.** `binary` means we build, sign and host it — this requires a redistributable license. `recipe` means we cannot legally host the bits, so the recipe fetches from the vendor with a pinned checksum and runs on the user's machine. The requester declares intent; the factory verifies. A `binary` request for a license that is not on the redistributable allowlist is rejected with that reason rather than silently downgraded, because the requester should know which surface they are getting.

### `[source]`

| Field | Required | Description |
|---|---|---|
| `kind` | yes | `release` (a published release artifact), `tag` (build from a signed or annotated tag), or `vendor` (a vendor-hosted download; only valid with `surface = "recipe"`). |
| `version` | no | Pin to a specific version. Omit to take the latest release. |

There is deliberately no `-git` kind. A branch package has no pinned hash by definition: it is a moving target, it cannot be reproduced, and its content can change between review and build. If we support `-git` at all it belongs on the recipe surface, explicitly labelled unverified, and never as a signed binary in stable. Flagged as an open question below.

### `[requester]`

| Field | Required | Description |
|---|---|---|
| `relationship` | yes | `user` or `upstream_author`. A **claim**, verified separately. |
| `notes` | no | Plain text for the reviewing maintainer. Max 2000 chars. |

`relationship = "upstream_author"` triggers identity verification against the `upstream` repository. Verified upstream authorship is what unlocks automatic promotion for subsequent version bumps: it places the requester inside the trust exception we already grant upstream, rather than creating a new one.

Verification proves control of an account that owns the repository. It does not prove authorship, and it does not survive a repository transfer, an organisation member with write access, or a compromised account. It is a strong signal, not a certificate.

## What a request must never contain

Rejected outright, not sanitised:

- Build steps, `prepare`/`build`/`package` functions, or any shell
- Patches, or references to patches
- Dependency lists — the factory derives these
- Checksums — the factory computes and pins these
- Compiler or linker flags
- Install scripts, hooks, or systemd units
- Any field not listed in this document

The last rule matters most. Validation fails closed on unknown keys, so the format cannot be extended by smuggling.

**Invariant:** the factory treats every value in a request as data. No request value is ever interpolated into a shell command. `name` reaches `makepkg` as a validated identifier, never as a string spliced into bash.

## Validation

Runs on every PR, before a human reads the request. No network access beyond resolving `upstream`, no secrets, no code execution.

1. `schema` is a known version
2. No unknown keys at any level
3. `name` matches the pattern and length limit
4. `name` does not collide with an existing OPR package or with Arch `core`/`extra`
5. `name` is at least edit-distance 2 from any package in the top-N catalogue, or the request is flagged for typosquat review
6. `upstream` is HTTPS, resolves, is a repository root, and does not redirect off-host
7. `license` is a valid SPDX expression
8. If `surface = "binary"`, `license` is on the redistributable allowlist
9. `architectures` is a non-empty subset of the supported set
10. `source.kind = "vendor"` implies `surface = "recipe"`
11. `description` and `notes` are within length limits and contain no control characters

Rule 5 is worth calling out: name-similarity attacks are a real AUR failure mode and this is the cheapest place to catch them.

## The verification block

Written by the pipeline, never by the requester. A PR that adds or modifies `[verification]` is rejected.

```toml
[verification]
verified_at = "2026-09-04T10:14:00Z"
upstream_resolved = "https://github.com/ghostty-org/ghostty"
license_detected = "MIT"
requester_identity = "github:someuser"
relationship_verified = true
```

This turns the request file into the durable record for the package: what was claimed, what was checked, and when. Reviewers read one file rather than correlating a PR with a CI log.

## Open questions for reviewers

1. **Issue form as the front door.** A PR requires a fork and a clone. An issue form is lower friction and produces the same structured data; a bot opens the PR. Worth it, or unnecessary machinery?
2. **`-git` packages.** Recipe surface only, or excluded entirely for now?
3. **Monorepo vs per-package repositories.** This schema works either way. Per-package repos give better isolation and ownership; a monorepo makes cascading rebuilds and mass fixes tractable. Phase 1 works fine as a monorepo, but the decision should be deliberate.
4. **Should `license` be requester-declared at all?** The factory detects it anyway. The argument for keeping it is that a mismatch between claim and detection is itself a useful signal.
16 changes: 16 additions & 0 deletions requests/ghostty.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
schema = 1

[package]
name = "ghostty"
description = "GPU-accelerated terminal emulator"
upstream = "https://github.com/ghostty-org/ghostty"
license = "MIT"
surface = "binary"
architectures = ["x86_64", "aarch64"]

[source]
kind = "release"

[requester]
relationship = "user"
notes = "Widely used in the Omarchy community; currently only available via AUR."
16 changes: 16 additions & 0 deletions requests/google-chrome.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
schema = 1

[package]
name = "google-chrome"
description = "Web browser from Google"
upstream = "https://dl.google.com/linux/chrome/deb"
license = "LicenseRef-Google-Chrome-ToS"
surface = "recipe"
architectures = ["x86_64"]

[source]
kind = "vendor"

[requester]
relationship = "user"
notes = "Not redistributable, so recipe surface only. Recipe fetches from Google with a pinned checksum. Vendor rewrites the file at the same URL on release, so checksum drift is expected and must freeze the package for review rather than auto-update."
108 changes: 108 additions & 0 deletions schema/request.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://opr.omarchy.org/schema/request/1",
"title": "OPR packaging request",
"description": "Validated against the parsed TOML of requests/<name>.toml. Fails closed on unknown keys.",
"type": "object",
"additionalProperties": false,
"required": ["schema", "package", "source", "requester"],
"properties": {
"schema": {
"type": "integer",
"const": 1
},
"package": {
"type": "object",
"additionalProperties": false,
"required": ["name", "description", "upstream", "license", "surface", "architectures"],
"properties": {
"name": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9@._+-]*$",
"maxLength": 64
},
"description": {
"type": "string",
"minLength": 1,
"maxLength": 120,
"pattern": "^[^\\n\\r]*$"
},
"upstream": {
"type": "string",
"format": "uri",
"pattern": "^https://",
"maxLength": 512
},
"license": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"surface": {
"type": "string",
"enum": ["binary", "recipe"]
},
"architectures": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {
"type": "string",
"enum": ["x86_64", "aarch64"]
}
}
}
},
"source": {
"type": "object",
"additionalProperties": false,
"required": ["kind"],
"properties": {
"kind": {
"type": "string",
"enum": ["release", "tag", "vendor"]
},
"version": {
"type": "string",
"maxLength": 64
}
}
},
"requester": {
"type": "object",
"additionalProperties": false,
"required": ["relationship"],
"properties": {
"relationship": {
"type": "string",
"enum": ["user", "upstream_author"]
},
"notes": {
"type": "string",
"maxLength": 2000
}
}
}
},
"allOf": [
{
"$comment": "A vendor-hosted source cannot be republished as a signed binary.",
"if": {
"properties": {
"source": {
"properties": { "kind": { "const": "vendor" } },
"required": ["kind"]
}
},
"required": ["source"]
},
"then": {
"properties": {
"package": {
"properties": { "surface": { "const": "recipe" } }
}
}
}
}
]
}
Loading