Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
b6bc698
feat(node): integrate the #173 IPFS CID tree-gate onto the #174 walk-…
beardthelion Jul 17, 2026
332a578
fix(node): close grok P1 findings on the #173/#174 IPFS integration
beardthelion Jul 17, 2026
dbadd24
fix(node): bound the /ipfs cat-file probes under the walk permit; bac…
beardthelion Jul 17, 2026
bb3ecc8
fix(node): tie the /ipfs walk ceiling to MAX_PIN_SOURCES + 1
beardthelion Jul 17, 2026
107abf9
fix(node): align ipfs_pin return with pinata; add Retry-After to Sear…
beardthelion Jul 17, 2026
1e2f0d1
fix(node): close the /ipfs pin-source griefing hole via a bounded sca…
beardthelion Jul 17, 2026
de2ad5a
fix(node): hold read admission through the filtered-pack reaper
beardthelion Jul 20, 2026
b881447
fix(node): own /ipfs admission for the life of the walk
beardthelion Jul 20, 2026
6222775
fix(node): requeue coalesced pushes instead of dropping them
beardthelion Jul 21, 2026
86fc276
fix(node): wire GITLAWB_IPFS_MAX_REPOS_WALKED to the legacy-probe budget
beardthelion Jul 21, 2026
20c338e
fix(node): split /ipfs work accounting off the route rate limiter
beardthelion Jul 21, 2026
c04c4a5
fix(node): retry pin-source and pinned-CID recording on transient errors
beardthelion Jul 21, 2026
15bdc5e
fix(node): opportunistically repair legacy provider-CID pins
beardthelion Jul 21, 2026
9cef54e
fix(review): share one deadline across the /ipfs size+content reads
beardthelion Jul 21, 2026
cc41d37
fix(node): bound the post-push pin and repair reads so a wedge cannot…
beardthelion Jul 21, 2026
a4f7d5d
fix(node): own the repo write lock on one connection so cancellation …
beardthelion Jul 27, 2026
2961ba7
fix(node): retry the requeue re-read so a coalesced push is not silen…
beardthelion Jul 27, 2026
ff189ce
fix(node): record a failed pin-source write so the resolver keeps its…
beardthelion Jul 27, 2026
412d04e
fix(node): sweep legacy provider-CID pins instead of waiting for a re…
beardthelion Jul 27, 2026
752fd19
fix(node): stop the lock pool from being occupied by spinners and rac…
beardthelion Jul 27, 2026
ef9b716
fix(node): keep the incompleteness marker until a source is really re…
beardthelion Jul 27, 2026
d854161
fix(node): shed 503 from the issue and PR write paths when the lock p…
beardthelion Jul 28, 2026
219fbdb
test(node): cover the sweep's boot wiring and prove an interrupted pu…
beardthelion Jul 28, 2026
e5c1da2
Merge #174 into the CID tree-gate branch
beardthelion Aug 2, 2026
3ca35d2
test(node): restore three guard tests the #174 merge dropped
beardthelion Aug 2, 2026
979d3c4
test(node): restore the four u3 drain tests onto the coalescer
beardthelion Aug 2, 2026
8557a12
test(node): add a test-only fault seam for the drain's two re-reads
beardthelion Aug 2, 2026
d1fc9bc
fix(node): retry the coalesced drain's re-read instead of losing the …
beardthelion Aug 3, 2026
aeb6895
test(node): stop the repos.rs structural gates splitting at the wrong…
beardthelion Aug 3, 2026
a0e0e0b
Merge remote-tracking branch 'origin/fix/served-git-concurrency-cap' …
beardthelion Aug 3, 2026
9bba8f9
test(node): restore the ipfs-max-repos-walked range test the #174 mer…
beardthelion Aug 3, 2026
9f74811
Merge origin/main into fix/issue-135-ipfs-cid-tree-gate
beardthelion Aug 10, 2026
8f3a521
docs(node): correct the walk-cap and db-pool operator docs
beardthelion Aug 10, 2026
f839fb9
fix(node): stop reporting a Kubo pin whose DB record never landed
beardthelion Aug 10, 2026
4681f54
fix(node): bring the Pinata pin path back into lockstep with the Kubo…
beardthelion Aug 10, 2026
ea2219b
fix(node): bound the legacy-repair read by the pin batch deadline
beardthelion Aug 10, 2026
208fd80
Merge remote-tracking branch 'origin/main' into fix/issue-135-ipfs-ci…
beardthelion Aug 10, 2026
bad82b3
fix(node): clamp every admission-held DB await in the CID resolver
beardthelion Aug 10, 2026
9984e08
fix(node): bound the pin loops' DB work by the batch deadline, both l…
beardthelion Aug 11, 2026
9ac3fd3
Merge origin/main into fix/issue-135-ipfs-cid-tree-gate
beardthelion Aug 11, 2026
1b44b12
fix(node): rewind the pin repair sweep cursor on every completed walk
beardthelion Aug 11, 2026
e4aaae8
fix(node): bound the fruitless reads one sweep run will spend
beardthelion Aug 11, 2026
ed6355e
fix(node): stop the /ipfs size probe reporting a fault as a missing o…
beardthelion Aug 11, 2026
5575da4
fix(node): make the pin-source incompleteness marker per (object, repo)
beardthelion Aug 11, 2026
9758bc7
docs(node): name the knob that actually sets the legacy-probe budget
beardthelion Aug 11, 2026
4985e38
fix(ipfs): page the legacy CID scan instead of materializing every repo
beardthelion Aug 12, 2026
d88c0c7
fix(node): repair source-less legacy pins by bounded additive discovery
beardthelion Aug 11, 2026
25cab14
test(node): pin the per-object marker's third-repo residual
beardthelion Aug 11, 2026
bd0bef1
fix(node): carry discovery onto the per-(oid, repo) incompleteness ma…
beardthelion Aug 11, 2026
bc20236
fix(node): charge discovery's probes against the per-run read budget
beardthelion Aug 11, 2026
4c665fb
fix(node): port discovery's candidate load onto the paged repo query
beardthelion Aug 12, 2026
650460a
test(node): cover the pre-provenance upgrade row end to end
beardthelion Aug 12, 2026
22560b0
fix(node): give each discovery row its own slice of the pass budget
beardthelion Aug 12, 2026
4fdfa1c
fix(node): rotate discovery candidates across traversals
beardthelion Aug 12, 2026
9babb5e
fix(node): record a discovered source and its marker in one commit
beardthelion Aug 12, 2026
22f4db9
fix(node): bound the legacy CID scan and let the caller carry its pos…
beardthelion Aug 12, 2026
ae96ffa
fix(node): budget provenance and fallback walks separately
beardthelion Aug 12, 2026
0d7711c
fix(node): bound the pre-walk CID resolve on its own short budget
beardthelion Aug 12, 2026
342dd48
fix(review): mint a continuation at every ceiling and widen the token
beardthelion Aug 13, 2026
e6cfb58
fix(review): bound token key, rule bytes, and the discovery load
beardthelion Aug 13, 2026
f4f0f4b
Merge remote-tracking branch 'origin/main' into fix/issue-135-ipfs-ci…
beardthelion Aug 13, 2026
75c0e28
test(node): probe the production advisory-lock key, not a stale copy
beardthelion Aug 13, 2026
a60026b
test(node): pin the legacy scan's DB-facing row selection to the conf…
beardthelion Aug 13, 2026
21e6873
fix(node): bound the legacy scan's query by the remaining row budget
beardthelion Aug 13, 2026
1d37cb1
test(gl): cover the scan-continuation resume ladder in gl ipfs get
beardthelion Aug 13, 2026
9a8d190
fix(gl): follow scan continuations in gl ipfs get, bounded three ways
beardthelion Aug 13, 2026
181f617
fix(gl): accept the resume token the incomplete-scan message tells yo…
beardthelion Aug 13, 2026
eac4d43
docs(gl): describe how gl ipfs get resumes a truncated scan, and its …
beardthelion Aug 13, 2026
8c39cfd
fix(review): keep the resume token on every terminal that still holds…
beardthelion Aug 13, 2026
de3f230
fix(gl): stop carrying request signatures to a host the node redirect…
beardthelion Aug 13, 2026
ba44a01
fix(core): share the origin-scoped redirect rule with the git helper
beardthelion Aug 13, 2026
c82a70a
fix(core): put the redirect predicate behind a feature so core stays …
beardthelion Aug 13, 2026
5c0d6f3
fix(core): refuse a redirect that rewrites the signed request-target
beardthelion Aug 14, 2026
92bb554
test(clients): prove the refused redirect never reaches the node veri…
beardthelion Aug 14, 2026
1c86ee2
test(node): pin require_signature to the request-target it received
beardthelion Aug 14, 2026
dc5daa1
fix(node): size the ipfs work floor for provenance walks plus one ful…
beardthelion Aug 14, 2026
a7e8e35
fix(review): make the coupled sites findable and pin two more properties
beardthelion Aug 15, 2026
69cfbd4
fix(node): mint a continuation when a ceiling stops the /ipfs scan mi…
beardthelion Aug 15, 2026
42e6aab
Merge origin/main into fix/issue-135-ipfs-cid-tree-gate
beardthelion Aug 15, 2026
1bdefb1
chore(node): log every /ipfs scan truncation and correct the helper's…
beardthelion Aug 16, 2026
366b599
fix(node): order the CID candidate list so it cannot depend on heap o…
beardthelion Aug 16, 2026
2656718
feat(core): carry the resumed candidate's identity in the scan token
beardthelion Aug 16, 2026
d0635d9
fix(node): resume the /ipfs scan per candidate so a starved one is no…
beardthelion Aug 16, 2026
76f6300
fix(node): never hand back a continuation that made no progress
beardthelion Aug 16, 2026
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
27 changes: 26 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,26 @@ GITLAWB_MAX_CONCURRENT_IPFS_WALKS=32
# per-caller caps via GITLAWB_TRUSTED_PROXY; reject-before-insert bounded map).
# Default 4.
GITLAWB_IPFS_WALK_PER_SOURCE=4
# Max legacy (NULL-provenance) repos probed per single /ipfs request, bounding the
# scan-fallback fan-out (git cat-file per candidate repo) for an anonymous caller. A
# truncated scan sheds a retryable 503, never a false 404. Default 256.
GITLAWB_IPFS_MAX_LEGACY_PROBES=256
# Max repo ROWS one /ipfs request's legacy scan may read from the database. The probe
# ceiling above only counts once a probe runs, and quarantined or private repos are
# denied before that, so without this an all-denying inventory paged the whole repo
# table for one anonymous request. A truncated scan sheds a retryable 503 carrying a
# sealed `continuation` token; echo it as ?scan= to resume, so a holder buried past the
# ceiling is reachable in ceil(repos / ceiling) + 1 requests. Raising this also raises
# the per-caller /ipfs work allowance, since each page is charged to it. Lowering it
# sharpens a coarse oracle: laddering to the end reveals the node's total repo count to
# within one ceiling. Default 2048.
GITLAWB_IPFS_MAX_LEGACY_SCAN_ROWS=2048
# Max EXPENSIVE path-scope visibility walks per single /ipfs request (only a
# blob in a path-scoped repo costs a full-history walk). Over-cap repos are
# skipped without a verdict and the scan continues; if the object is then found
# nowhere the request sheds a retryable 503 instead of a false 404. Default 64.
# nowhere the request sheds a retryable 503 instead of a false 404. The effective
# cap is the tighter of this value and the node's internal per-request ceiling of
# 17, so values above 17 have no effect; lower values do tighten it. Default 64.
GITLAWB_IPFS_MAX_REPOS_WALKED=64
# Ceiling on repos one /ipfs request may VISIT past the visibility gate. Each
# visit costs a repo acquire — on a Tigris cache miss a full archive download,
Expand All @@ -237,6 +253,15 @@ GITLAWB_IPFS_MAX_REPO_VISITS=1024
# Must be 1..=3153600000 (100 years): the node derives an Instant deadline from
# this value, and a larger one cannot be represented. Default 600.
GITLAWB_IPFS_REQUEST_BUDGET_SECS=600
# Shorter budget (seconds) for the pre-walk CID resolve: the lookup that maps a
# requested CID to its git oid(s), which runs while the scarce walk admission is
# already held. A well-formed CID with no pin row does no probe and no walk work,
# so without this a stalled lookup could hold a walk slot for the whole request
# budget while nothing walked. The effective deadline is the lesser of this and
# the remaining request budget; walk and probe work stay on the request budget,
# so a slow but progressing scan is never shed by it.
# Must be 1..=3153600000 (100 years). Default 10.
GITLAWB_IPFS_RESOLVE_BUDGET_SECS=10
# Max /ipfs/{cid} requests per client IP per hour (route flood brake, distinct
# from the concurrency caps above). 0 disables. Default 600.
GITLAWB_IPFS_RATE_LIMIT=600
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4"] }
# http client
reqwest = { version = "0.12", features = ["blocking", "json", "multipart", "rustls-tls"], default-features = false }
# URL parsing (what reqwest::Url re-exports, so the shared redirect predicate can
# take a parsed URL without pulling reqwest into gitlawb-core)
url = "2"
# HMAC
hmac = "0.12"

Expand Down
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,53 @@ Notes:
sends the bearer token **only** to your configured origin, never to a URL a
node advertises, so a hostile node can't capture the key or redirect the solve.

### Fetching an object by CID

```bash
gl ipfs list # CIDs this node has pinned
gl ipfs get bafkrei... > object.bin # object bytes on stdout
```

Objects that were pinned before the node started recording which repo they came
from are found by scanning its repo inventory, and that scan stops at the
per-request ceilings in the [Configuration](#configuration) table. A stopped scan
answers 503 with a resume token instead of a false "not found", and `gl ipfs get`
follows the token automatically: up to 8 resumes after the first request, so at
most 9 calls to the node, waiting between attempts for as long as the node's
`Retry-After` asks and never longer than 5 seconds.

The whole ladder runs under a 60 second wall-clock deadline. The deadline bounds
the search, not the download: each attempt gets the time left on it to produce
response headers, and once an object is found its bytes stream outside that
deadline. They are not unbounded, though. The client's own 30 second HTTP timeout
is a total request timeout, running from the start of a request until its body
has finished, so a transfer still going 30 seconds after its request began is cut
off. Waits between attempts never run past the deadline either, so a single run
spends at most around 90 seconds on the network: the deadline plus the 30 second
timeout covering the last attempt. Writing the object out sits outside both
bounds, so piping into a reader that stops reading can hold the command open
longer than that.

Two node-side brakes end a ladder early and are reported rather than retried
around. A 429 is terminal, because the node's rate-limit window is an hour and
that wait cannot be honored inside one invocation; a transient overload (a 503
carrying no incomplete-scan code) is retried on the token already held, under the
same cap, clamp and deadline. The per-IP fanout brake can also stop a ladder well
short of the 9 calls, so automatic resumption is not a guarantee of reaching the
object.

When a bound stops the ladder with a usable token in hand, the command prints the
token and the invocation that continues from it before exiting nonzero:

```txt
resume from where this stopped: gl ipfs get bafkrei... --scan <token>
```

Run that to carry on from where the scan stopped. Re-running without `--scan`
restarts at the first row, reproduces the same truncation and spends the node's
per-IP budget again, so the token is the only thing that makes progress. Tokens
are valid for an hour.

---

## Architecture
Expand Down Expand Up @@ -352,16 +399,21 @@ Important node settings:
| `GITLAWB_REPO_LEASE_MAX_WAITERS` | Max pushes parked at once waiting for the same repo's write lease. Each waiter pins its buffered pack body, so this bounds that memory for a hot repo; past the cap the newest push sheds a 503 + Retry-After instead of queueing. Pushes to other repos are unaffected, and the lease holder is not counted. Default 8. |
| `GITLAWB_MAX_CONCURRENT_IPFS_WALKS` | Max concurrent `GET /ipfs/{cid}` visibility walks across all callers (own pool, disjoint from the served-git pools); over-cap sheds 503. Default 32. |
| `GITLAWB_IPFS_WALK_PER_SOURCE` | Max concurrent `/ipfs` walks a single source IP may hold. Default 4. |
| `GITLAWB_IPFS_MAX_REPOS_WALKED` | Max expensive path-scope visibility walks per `/ipfs/{cid}` request; over-cap repos are skipped and the scan continues, shedding a retryable 503 (not a false 404) if the object is then found nowhere. Default 64. |
| `GITLAWB_IPFS_MAX_LEGACY_PROBES` | Max legacy (NULL-provenance) repos probed per `/ipfs/{cid}` request, bounding the scan-fallback fan-out. A truncated scan returns a retryable 503, not a false 404. Default 256. |
| `GITLAWB_IPFS_MAX_LEGACY_SCAN_ROWS` | Max repo rows one `/ipfs/{cid}` request's legacy scan may read from the database. The probe ceiling above only starts counting once a probe runs, and quarantined or private repos are denied before that, so this is what bounds a scan over an inventory that denies the caller everywhere. A truncated scan sheds a retryable 503 carrying an opaque `continuation` token; echoing it as `?scan=` resumes the scan where it stopped. Every per-request ceiling on this path (rows, probes, visits, retained rule bytes) mints one, so each request advances the ladder by at least the rows it read and a holder buried past a ceiling is reached in a bounded number of requests, `ceil(repos / ceiling) + 1` when this row ceiling is the one that binds. No ceiling ever produces a 404. Every page is charged to the caller's `/ipfs` work allowance, so raising this raises that allowance too. Lowering it sharpens an oracle: because a truncation emits a token and a completed wrap does not, laddering to the end reveals the node's total repo count (private and quarantined included) to within one ceiling. Default 2048. |
| `GITLAWB_IPFS_MAX_REPOS_WALKED` | Max expensive path-scope visibility walks a `/ipfs/{cid}` request may run per phase; over-cap repos are skipped and the scan continues, shedding a retryable 503 (not a false 404) if the object is then found nowhere. The effective cap is the tighter of this knob and the node's internal history-walk ceiling of 17 (`MAX_PIN_SOURCES + 1`), so a value above 17 has no effect while a value below it does tighten the cap. It is charged per phase: the provenance lookup and the legacy-scan fallback get separate equal budgets, so one request can run up to twice the cap in total. Default 64. |
| `GITLAWB_IPFS_MAX_REPO_VISITS` | Ceiling on repos one `/ipfs/{cid}` request may visit (acquire + probe) past the visibility gate. Also the worst-case per-request Tigris fetch count. On exhaustion the scan stops with a retryable 503. Default 1024. |
| `GITLAWB_IPFS_REQUEST_BUDGET_SECS` | Absolute wall-clock budget for one admitted `/ipfs/{cid}` request's acquire+walk lifetime. Per-stage clamps bound the acquire and walk stages to the remaining budget, and no stage starts once it is exhausted; the scan then stops with a retryable 503. The object-type probe and content-read `cat-file` subprocesses are budget-checked before starting and each also run under their own deadline (the lesser of `GITLAWB_GIT_SERVICE_TIMEOUT_SECS` and the remaining budget), reaped via process-group teardown, so a hung `cat-file` cannot hold the request's walk slot past it. One hang path is still unbounded: the probe's object-store readability check is a plain filesystem sweep with nothing to reap, so a wedged filesystem can hold the slot past the deadline. Default 600. Accepted range is 1 to 3153600000 (100 years), since the node derives a deadline from this value and a larger one cannot be represented. |
| `GITLAWB_IPFS_RESOLVE_BUDGET_SECS` | Shorter budget for the pre-walk CID resolve inside an admitted `/ipfs/{cid}` request: the lookup that maps the requested CID to its git oid(s), which runs while the scarce walk admission is already held. A well-formed CID with no pin row does no probe and no walk work, so without this it could hold a walk slot for the whole request budget while nothing walked, and enough such requests shed every real retrieval at admission. The effective deadline is the lesser of this and the remaining request budget, so a value above `GITLAWB_IPFS_REQUEST_BUDGET_SECS` degrades to the request budget. Only the resolve is on this clock; walk and probe work stay on the request budget, so a slow but progressing scan is never shed by it. Default 10. Accepted range is 1 to 3153600000 (100 years). |
| `GITLAWB_IPFS_RATE_LIMIT` | Max `/ipfs/{cid}` requests per client IP per hour (route flood brake). 0 disables. Default 600. |
| `GITLAWB_TIGRIS_BUCKET` | Optional S3/Tigris shared repo storage bucket. |
| `GITLAWB_PINATA_JWT` | Optional Pinata/IPFS warm-storage pinning. |
| `GITLAWB_IRYS_URL` | Optional Irys/Arweave permanent anchoring. |

Production note: change the default Postgres password before exposing a node publicly.

Legacy-pin window: releases before the CID-resolver work stored the provider CID (Kubo dag-pb / Pinata) as a pinned object's resolver key. The `/ipfs/{cid}` resolver now recomputes the raw-content CID from the object bytes and refuses to serve a key that does not match, so `GET /api/v1/ipfs/pins` can still advertise an unrepaired legacy CID that 404s. Such a row is repaired opportunistically the next time a push carries the object again (its key is rewritten to the raw CID, the old value kept in `legacy_provider_cid`), but git negotiation omits objects the node already has, so most legacy rows never re-enter a push delta. A deferred one-shot startup sweep, not this opportunistic path, is what fully retires the advertise-then-404 window. Rows whose object bytes are gone stay withheld.

---

## Optional node staking
Expand Down
2 changes: 1 addition & 1 deletion crates/git-remote-gitlawb/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ name = "git-remote-gitlawb"
path = "src/main.rs"

[dependencies]
gitlawb-core = { path = "../gitlawb-core" }
gitlawb-core = { path = "../gitlawb-core", features = ["redirect"] }
anyhow = { workspace = true }
reqwest = { workspace = true }
tracing = { workspace = true }
Expand Down
Loading
Loading