docs(grasp06): flip status to implemented and add operator how-to

Reconcile the design document with the shipped implementation and add
the operator-facing documentation for enabling and verifying the
feature.

- docs/explanation/grasp-06-contributor-pr-submission.md: flip the
  header from 'PLANNED — NOT YET IMPLEMENTED' to 'Implemented'.
  Document the per-(submitter, identifier) tokio mutex around
  on-demand git init, the strict clone-URL comparator in
  src/grasp06/policy.rs, the deferred announcement back-fill on the
  cross-service mirror, and the periodic /prs/ cleanup sweep.

- docs/explanation/architecture.md: add a Contributor PR Submission
  subsection pointing at the design doc and listing the module layout
  under src/grasp06/.

- docs/how-to/enable-grasp-06.md: new task-oriented how-to. Covers
  flipping the flag, verifying NIP-11 advertises GRASP-06, verifying
  /prs/ is reachable with git clone, storage cost, and the abuse
  controls that exist today vs. those scheduled for future releases.

- CHANGELOG.md: user-facing summary under [Unreleased].
This commit is contained in:
DanConwayDev
2026-05-15 18:16:22 +00:00
parent 39c21b29e6
commit a561efa8d9
4 changed files with 136 additions and 4 deletions
+4
View File
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- **GRASP-06 contributor PR submission endpoint** (`NGIT_GRASP06_ENABLE`, default off). When enabled, the relay accepts unauthenticated `git push` of `refs/nostr/<event-id>` to `/prs/<npub>/<identifier>.git` from any contributor, even for repositories this relay has no accepted announcement for. The corresponding PR (kind 1618) or PR Update (kind 1619) event is accepted into purgatory when its `clone` tag names this relay's `/prs/<signer>/<d>.git` endpoint and its `a` tag's d-tag matches the URL identifier. When the event and the push match (signer, d-tag, c-tag commit) the event is released from purgatory and the ref is mirrored into any accepted-announcement repos on this relay. Empty `/prs/` repos (probe pushes, mismatched events) are garbage-collected by the receive handler and by a periodic 10-minute sweep. GRASP-06 is advertised in NIP-11 `supported_grasps` when enabled. See [how-to/enable-grasp-06.md](docs/how-to/enable-grasp-06.md) and [explanation/grasp-06-contributor-pr-submission.md](docs/explanation/grasp-06-contributor-pr-submission.md).
## [1.0.2] - 2026-04-10
### Fixed
+15
View File
@@ -545,6 +545,21 @@ Negentropy Sync
**Source Code:** [`src/sync/rejected_index.rs`](../../src/sync/rejected_index.rs)
## Contributor PR Submission (GRASP-06)
Optional endpoint at `/prs/<npub>/<identifier>.git`, gated on `NGIT_GRASP06_ENABLE` (default off). Contributors push `refs/nostr/<event-id>` for PR and PR Update events targeting any repository — even repos this relay has no accepted announcement for. The endpoint is unauthenticated at the HTTP level; validity is established by the signed PR / PR Update event and inline acceptance rules.
**Module layout:**
- [`src/grasp06/endpoint.rs`](../../src/grasp06/endpoint.rs) — URL parsing.
- [`src/grasp06/paths.rs`](../../src/grasp06/paths.rs) — on-disk path conventions under `<git_data_path>/prs/<hex>/<identifier>.git`.
- [`src/grasp06/fetch.rs`](../../src/grasp06/fetch.rs) — empty-repo synthesis for `info/refs` and `git-upload-pack` against repos that don't yet exist on disk.
- [`src/grasp06/receive.rs`](../../src/grasp06/receive.rs) — `git-receive-pack` with init-on-push, strict `refs/nostr/<event-id>` ref-name validation, and per-ref post-push validation against the database and purgatory.
- [`src/grasp06/policy.rs`](../../src/grasp06/policy.rs) — strict clone-tag URL comparator used by the PR-event acceptance relaxation.
- [`src/grasp06/cleanup.rs`](../../src/grasp06/cleanup.rs) — periodic sweep that removes zero-ref `/prs/` repos older than the purgatory TTL.
`/prs/` repos are intentionally isolated from other subsystems: empty-repo cleanup skips the `/prs/` subtree, the proactive-sync subsystem never discovers them because subscriptions are built from DB-resident announcements, and the standard repo landing page guards against ever matching a `/prs/` path. Full design: [GRASP-06 Contributor Pull Request Submission](grasp-06-contributor-pr-submission.md). Operator how-to: [Enable GRASP-06](../how-to/enable-grasp-06.md).
## Future Extensions
### GRASP-02: Proactive Sync
@@ -1,9 +1,10 @@
# GRASP-06: Contributor Pull Request Submission — Design
**Status**: 🚧 **PLANNED — NOT YET IMPLEMENTED** 🚧
**Status**: Implemented (opt-in via `NGIT_GRASP06_ENABLE`)
**Spec**: [GRASP-06](https://github.com/DanConwayDev/grasp/blob/main/06.md)
**Related**: [Purgatory Design](purgatory-design.md), [Architecture](architecture.md), [Inline Authorization](inline-authorization.md)
**Operator how-to**: [Enable GRASP-06](../how-to/enable-grasp-06.md)
---
@@ -104,6 +105,10 @@ POST /prs/<npub>/<id>.git/git-receive-pack
The flow mirrors the existing `refs/nostr/<event-id>` path at the standard endpoint (see [`src/git/handlers.rs:handle_receive_pack`](../../src/git/handlers.rs) and the PR purgatory entries in [`src/purgatory/types.rs`](../../src/purgatory/types.rs)) — it just skips the `authorize_push` path that checks the maintainer set, and applies the spec's validation invariant instead.
#### On-demand bare repo creation
The first push to `/prs/<submitter>/<identifier>.git` creates the bare repo on disk. A per-`(submitter, identifier)` `tokio::sync::Mutex` (kept in a `DashMap` on the `HttpService`, see [`crate::grasp06::receive::RepoInitLocks`](../../src/grasp06/receive.rs)) serialises only the `git init --bare` step. Once init has succeeded, git's own ref locking handles intra-push concurrency, so simultaneous pushes to the same path proceed in parallel.
### Event acceptance relaxation
The existing PR event policy in [`src/nostr/policy/pr_event.rs`](../../src/nostr/policy/pr_event.rs) calls `fetch_repository_data_excluding_purgatory` to require an accepted announcement in the database before accepting a PR event. Under GRASP-06 this check is loosened for events that satisfy:
@@ -116,6 +121,8 @@ Such events skip the "references accepted announcement" check and are accepted i
Events that qualify under the existing GRASP-01 rules still flow through the normal path unchanged — the GRASP-06 branch is only taken when the existing path would have rejected.
The clone-URL match is implemented in [`src/grasp06/policy.rs`](../../src/grasp06/policy.rs) as a strict comparator: it requires `http`/`https` scheme, an exact (case-insensitive) authority match against `config.domain`, no query string or fragment, exactly two path segments `<npub-segment>/<repo-segment>.git`, the npub segment decoding via `PublicKey::from_bech32` to the event's signer, and the percent-decoded identifier matching one of the event's `a`-tag `<d>` values. Anything else fails the relaxation and the event falls through to the existing rejection path.
### Cross-service mirror
When a PR or PR Update's purgatory entry is released via a `/prs/` push:
@@ -127,6 +134,10 @@ When a PR or PR Update's purgatory entry is released via a `/prs/` push:
The mirror copies the same ref, same commits. No separate object store. Dedup can be added transparently later via git alternates keyed on d-tag.
The mirror is **one-directional**: pushes to `/<maintainer>/<id>.git` are not mirrored into `/prs/*`. Only the `/prs/` → `<maintainer>/` direction fires, and only when the source repo path is under `prs_base_path`.
The mirror also does **not back-fill** retroactively. If an announcement for one of the event's `a` coords is accepted *after* a matching `/prs/` push has already happened, the ref remains only at `/prs/<signer>/<d>.git`; clients can still fetch it via the event's `clone` tag. Back-filling on announcement promotion is deferred — the simplest correct shape ships first, and the spec allows clients to resolve the PR through `/prs/` indefinitely.
### Purgatory integration
No new purgatory entry types are strictly required. The existing `PrPurgatoryEntry` already supports event-first and git-first patterns keyed by event id. Under GRASP-06:
@@ -138,9 +149,20 @@ Placeholder entries created at `/prs/` should be validated against `(submitter =
### Exclusion from other subsystems
- **Empty-repo cleanup** ([`src/cleanup_empty_repos.rs`](../../src/cleanup_empty_repos.rs)): skip `<git_data_path>/prs/*`. The `/prs/` endpoint has its own cleanup rule (zero-refs repos, expired placeholders).
- **Proactive sync** (GRASP-02): `/prs/` repos are not replicated between relays. A GRASP-06 relay is authoritative for the PRs it accepts. Clients that need a PR should fetch it from the relay the event's `clone` tag names.
- **Repo listings / NIP-11**: `/prs/` repos are not advertised as repositories. They are a submission side-channel, not first-class hosted repos.
- **Empty-repo cleanup** ([`src/cleanup_empty_repos.rs`](../../src/cleanup_empty_repos.rs)): skips `<git_data_path>/prs/*` via [`is_prs_repo_path`](../../src/grasp06/paths.rs) before recursing, so contributor-submission repos cannot be misreported as orphans.
- **Repo landing pages** ([`src/http/mod.rs::parse_repo_url`](../../src/http/mod.rs)): refuses any path starting with `/prs/` as a defensive guard, regardless of `grasp06_enable`. The `HttpService` routing also intercepts `/prs/*` earlier when the feature is on.
- **Proactive sync** (GRASP-02): `/prs/` repos are not replicated between relays. The proactive-sync subsystem derives every subscription from the DB-resident announcement set; `/prs/` repos have no announcement and so are excluded by construction. No filesystem walk discovers them. A GRASP-06 relay is authoritative for the PRs it accepts. Clients that need a PR should fetch it from the relay the event's `clone` tag names.
- **Repo listings / NIP-11**: `/prs/` repos are not advertised as repositories. They are a submission side-channel, not first-class hosted repos. GRASP-06 itself is advertised in the relay's NIP-11 `supported_grasps` list when the flag is on.
### Periodic `/prs/` cleanup
In addition to the receive-handler's post-push zero-ref cleanup, a periodic sweep ([`src/grasp06/cleanup.rs`](../../src/grasp06/cleanup.rs)) walks `<git_data_path>/prs/` every ten minutes (one second under `NGIT_TEST=1`) and removes any `<hex>/<id>.git` directory that:
1. has zero refs,
2. has no active `PrPurgatoryEntry` scoped to `(submitter=<hex>, identifier=<id>)`, and
3. has a directory mtime older than the purgatory TTL ([`purgatory::DEFAULT_EXPIRY`](../../src/purgatory/mod.rs), 30 minutes).
The mtime check defends against deleting a repo out from under an in-flight push. Empty submitter directories are removed when the sweep empties them.
## Flow examples
+91
View File
@@ -0,0 +1,91 @@
# How to enable GRASP-06 contributor PR submission
GRASP-06 adds an opt-in endpoint at `/prs/<npub>/<identifier>.git` that any contributor can push PR (kind 1618) and PR Update (kind 1619) `refs/nostr/<event-id>` refs to, even when this relay has no accepted announcement for the target repository. The endpoint is unauthenticated at the HTTP level — validity is established by the signed PR event.
This page covers the operator-facing steps to turn it on and verify it.
## 1. Flip the feature flag
Set `NGIT_GRASP06_ENABLE=true` for the relay process. The flag is off by default; everything below is a no-op without it.
**Environment file** (`.env`):
```env
NGIT_GRASP06_ENABLE=true
```
**CLI flag**:
```bash
ngit-grasp --grasp06-enable ...
```
**NixOS module**:
```nix
services.ngit-grasp.instances.<name>.grasp06Enable = true;
```
Restart the relay after the change.
## 2. Verify NIP-11 advertises GRASP-06
NIP-11 is served from `/` with the `Accept: application/nostr+json` header:
```bash
curl -s -H 'Accept: application/nostr+json' https://your-relay.example/ | jq '.supported_grasps'
```
The output must include `"GRASP-06"`. If it does not, the flag did not take effect — re-check the env var name and that the relay actually restarted.
## 3. Verify `/prs/` is reachable
Pick any well-formed npub and any identifier. A non-existent contributor + repo combination is fine — the endpoint synthesises an empty bare repo for fetches against paths that don't yet exist:
```bash
git clone https://your-relay.example/prs/npub1.../any-identifier.git /tmp/probe
```
You should get a successful clone of an empty repository (zero refs). No directory is created on the server for this probe.
To verify a contributor push round-trip, publish a kind 1618 PR event whose `clone` tag names this relay's `/prs/<signer-npub>/<d>.git` URL, then:
```bash
git push https://your-relay.example/prs/<signer-npub>/<d>.git \
<commit-sha>:refs/nostr/<event-id>
```
The push succeeds, the relay creates `<git_data_path>/prs/<signer-hex>/<d>.git` on demand, and the ref is locked into the repo. Pushes to anything other than `refs/nostr/<64-lowercase-hex>` are rejected with an `ERR` pkt-line.
## Storage cost
One bare repo per `(submitter, identifier)` combination under `<git_data_path>/prs/<submitter-hex>/<identifier>.git`. Repos are garbage-collected automatically:
- After receive-pack, the repo is removed immediately if it has zero refs left (probe pushes that produced no valid state).
- A periodic sweep (every 10 minutes) removes any zero-ref `/prs/` repo with no active purgatory entry, older than the purgatory TTL.
There is no quota in this release. Disk consumption is bounded only by the rate at which contributors push valid PR refs.
## Abuse controls
The current release relies entirely on:
- the signed PR / PR Update event (no NIP-98 or other HTTP auth on push),
- the requirement that the event's `clone` tag names this relay's `/prs/<signer>/<d>.git` endpoint,
- the standard 30-minute purgatory TTL,
- post-push zero-ref cleanup, and
- the periodic `/prs/` sweep.
The following knobs are **future** additions and are not yet wired:
- per-submitter allowlist,
- per-submitter or per-event disk quotas,
- per-ref pack size cap,
- NIP-98 authenticated push,
- PoW gating.
If you need any of these today, leave `NGIT_GRASP06_ENABLE=false`.
## Spec
[GRASP-06 spec (draft)](https://github.com/DanConwayDev/grasp/blob/main/06.md). Design notes: [docs/explanation/grasp-06-contributor-pr-submission.md](../explanation/grasp-06-contributor-pr-submission.md).