mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 23:18:24 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
Reference in New Issue
Block a user