Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
2a8d64d
feat(core)!: verify_chain returns the proof chain's root issuer
Vasanthdev2004 Aug 14, 2026
13a2424
fix(node): gate two Unix-only tests so the crate compiles on Windows
Vasanthdev2004 Aug 14, 2026
1b1c972
feat(node): carry the verified UCAN and its root into the request
Vasanthdev2004 Aug 14, 2026
f8b9cf1
feat(node): add ucan_grants_push, anchored at the repo owner
Vasanthdev2004 Aug 14, 2026
d27f0be
feat(node): honour a delegated git/push capability on the push path
Vasanthdev2004 Aug 14, 2026
d141e59
feat(gl): gl ucan import stores a delegation for the push helper
Vasanthdev2004 Aug 14, 2026
3d81f57
feat(git-remote): send a delegated push capability as X-Ucan
Vasanthdev2004 Aug 14, 2026
8843c4f
test(git-remote): cover pack-POST URL splitting
Vasanthdev2004 Aug 14, 2026
a9e9c48
build: sync Cargo.lock for the git-remote-gitlawb dependencies
Vasanthdev2004 Aug 14, 2026
f455726
fix(core,gl,git-remote): close two review findings on delegated push
Vasanthdev2004 Aug 14, 2026
a2502fc
fix(core,node,gl,git-remote): close the second review round on delega…
Vasanthdev2004 Aug 14, 2026
d503655
fix(gl): restrict the delegations directory, and state the contract
Vasanthdev2004 Aug 14, 2026
5071524
fix(gl,git-remote): make a delegation issued the documented way actua…
Vasanthdev2004 Aug 15, 2026
5dfd8c5
fix(gl): refuse a relative or non-UTF-8 GITLAWB_KEY when deriving the…
Vasanthdev2004 Aug 15, 2026
e15fa73
fix(identity): one resolver for GITLAWB_KEY across gl and the helper
Vasanthdev2004 Aug 16, 2026
05fc52d
test(git-remote): cover delegation_header end to end
Vasanthdev2004 Aug 16, 2026
75aa1a0
fix(gl): finish the identity-path sweep on the write side
Vasanthdev2004 Aug 16, 2026
fad75a6
fix(gl,core): close round five — lazy home, envelope reads, honest re…
Vasanthdev2004 Aug 18, 2026
82f2484
fix(node,gl,git-remote): enforce repo narrowing at the node, honour t…
Vasanthdev2004 Aug 22, 2026
a47ea93
fix(gl): align the MCP ucan_show tool with gl ucan show
Vasanthdev2004 Aug 22, 2026
ec4399c
fix(gl): drop a needless borrow in quickstart's unix key write
Vasanthdev2004 Aug 22, 2026
1ab55e5
fix(node,gl,git-remote): fix a delegation's scope at issuance
Vasanthdev2004 Aug 22, 2026
89fa647
Merge origin/main into feat/ucan-push-authorization
Vasanthdev2004 Aug 27, 2026
9cf48f5
fix(gl): drop an unused import in the unix-only refresh tests
Vasanthdev2004 Aug 27, 2026
04e7941
fix(node): pass the UCAN extension at the last two receive-pack call …
Vasanthdev2004 Aug 27, 2026
f79309b
fix(core,node,gl,git-remote): one definition of a usable push capability
Vasanthdev2004 Sep 8, 2026
4dc62b2
fix(node): drop repo_capability_matches, orphaned by the shared push …
Vasanthdev2004 Sep 8, 2026
1da4251
fix(core,gl): bound every proof-chain walk and make verify report the…
Vasanthdev2004 Sep 8, 2026
fb831b1
docs: state the two gates a delegated push crosses, and finish the sweep
Vasanthdev2004 Sep 8, 2026
8b91f28
fix(gl): store one repository per imported delegation; pin the two-ga…
Vasanthdev2004 Sep 8, 2026
e3314c6
fix(gl,core): sanitize token-derived output, prove the publish step, …
Vasanthdev2004 Sep 8, 2026
1b5cb7d
test(gl): count the MCP tool list at 41 with ucan_import
Vasanthdev2004 Sep 8, 2026
e97a250
Merge remote-tracking branch 'origin/main' into pr-331
beardthelion Nov 10, 2026
9765cf0
fix(node,gl): reader gate on receive-pack POST, GITLAWB_KEY in option…
beardthelion Nov 10, 2026
bab62f6
test(gl): pin GITLAWB_KEY honouring in load_optional_keypair (#331)
beardthelion Nov 10, 2026
e75fad9
fix(gl): drop needless borrow caught by clippy
beardthelion Nov 10, 2026
14c0b50
fix(gl): doctor reports refused resolvers, sanitize MCP UCAN JSON, at…
beardthelion Nov 10, 2026
8686dce
fix(ucan): match push capabilities against the canonical repo name
beardthelion Nov 10, 2026
83aa56b
Merge remote-tracking branch 'origin/main' into pr-331
beardthelion Nov 10, 2026
464218c
fix(ucan): refuse key clobber on unreadable identity, fix doc gate si…
beardthelion Nov 10, 2026
de7ad7b
Merge remote-tracking branch 'origin/main' into pr-331
beardthelion Nov 10, 2026
7ad0b94
docs(node): gate 1 allow-list semantics, fix comment-strip order in f…
beardthelion Nov 10, 2026
d4b90b1
fix(node,gl): fail closed on protection-lookup errors, surface push r…
beardthelion Nov 10, 2026
f1a2cb4
fix(gl): judge push readiness with the shared push predicates in ucan…
beardthelion Nov 10, 2026
ea8c7f3
Merge remote-tracking branch 'origin/main' into pr-331
beardthelion Nov 10, 2026
cddb525
fix(git-remote): gate delegation_header on import's push predicates a…
beardthelion Nov 10, 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
17 changes: 13 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@
# Path to the node's Ed25519 keypair PEM file. Must name a key file, not a
# directory: a trailing slash, a final `.` or `..`, and `..` traversal are
# refused so the path cannot retarget a parent. A `~/` value must stay under
# home after expansion. Bare `identity.pem` is still legal.
# home after expansion, and the path must be absolute: its parent is also
# the delegation store that `gl ucan import` writes and `git-remote-gitlawb`
# reads, and the two do not share a working directory, so a relative path
# sends them to different places. Bare `identity.pem` under the default
# config dir is still legal.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
# Generate with: gl identity new
# Leaving it unset uses ~/.gitlawb/identity.pem, which is the shared default
# for both `gl` and `git-remote-gitlawb`. The bare-filename form is for the
# node's own service config; client tooling should inherit the default or use
# an absolute path.
GITLAWB_KEY=/data/keys/identity.pem

# Path to the node's persistent libp2p identity key file. Must include a
Expand Down Expand Up @@ -152,9 +160,10 @@ GITLAWB_REQUIRE_SIGNED_PEER_WRITES=false
# push to every repository, private ones included. On by default.
#
# Set to false only for a rolling upgrade whose pushers are not yet the repo
# owner. Note that delegated and CI keys count as non-owners: UCAN git/push is
# verified but not yet honored for authorization, so they cannot push while this
# is on. See docs/RUN-A-NODE.md.
# owner. A holder of an owner-rooted, time-bounded git/push UCAN naming that
# repository can push while this is on — see docs/RUN-A-NODE.md for issuing and
# importing one. A CI key with no such delegation counts as a non-owner and
# cannot push.
GITLAWB_ENFORCE_OWNER_PUSH=true

# Comma-separated libp2p multiaddrs.
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

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

10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ Good today:

Known limitations:

- Repository write authorization is secure by default: `GITLAWB_ENFORCE_OWNER_PUSH` defaults to `true`, so a valid HTTP Signature identifies a pusher and the node additionally requires that pusher to be the repository owner. Set `GITLAWB_ENFORCE_OWNER_PUSH=false` only for the duration of a rolling upgrade whose pushers are not yet the repo owner. Authentication is not authorization on its own (anyone can mint a `did:key` and sign), so owner enforcement is the only thing that prevents every signed caller from pushing to every repository, private ones included.
- UCAN proof chains are validated when supplied, but UCAN capabilities are not consulted by write authorization and the root issuer is not independently trust-anchored. UCANs therefore do not yet grant scoped collaborator access.
- Repository write authorization is one switch: `GITLAWB_ENFORCE_OWNER_PUSH`, which defaults to `true`. With it off — rolling upgrades only — a valid HTTP Signature identifies a pusher but does not authorize one, so every signed caller may push everywhere.
- UCAN capabilities are consulted on the push path only. There, the chain's root issuer is anchored to the repository owner, so an owner-rooted, time-bounded delegation that names the repository grants scoped collaborator access for pushing to it, for the `git/push`, `*`, or `repo/admin` actions. A `*` *resource* is refused: a delegation must name the repository it applies to, so it cannot grow to cover repos created later. Push-only cuts the other way too: a delegation grants no read access, so on a private repository the delegate must also be a reader (`gl visibility set`) — the two gates a push crosses are described in [`docs/RUN-A-NODE.md`](docs/RUN-A-NODE.md). The rest is unchanged: there is no revocation path, `nb` constraints are refused rather than interpreted, and no other route — reads, pull requests, issues, agents — consults capabilities at all.
- Agent lifecycle revocation is not enforced by HTTP Signature authorization; do not rely on removing or revoking an agent record to block a compromised signer.
- Task list/get reads (REST and GraphQL) are repository/task-gated and omit `ucan_token`. Slash-form `repo_id` rows (mirror-hosted tasks) can appear on list for the delegator or trimmed assignee, but `GET /api/v1/tasks/{id}` and GraphQL `task` fail closed for every caller because there is no local repository backing to serve. Read visibility is not a blanket data-classification boundary: IPFS-pin and Arweave-anchor listings are not repository-gated; withheld path names can be visible to a root reader; and later visibility changes cannot retract content already announced or externally anchored.
- Peer writes are signed by upgraded nodes, but strict signed-peer enforcement is opt-in during rolling upgrades.
Expand Down Expand Up @@ -313,7 +313,7 @@ metadata local disk / optional S3
| HTTP Signature | RFC 9421 signature proving control of the DID key for write requests. |
| Ref certificate | Signed record of a ref update. Useful for audit and replication. |
| Ref certificate versioning | The wire format is versioned; new certificates are currently issued as v1. `gl cert show <repo> <id> --verify` requires `--expect-node <did>`, and the queried node's self-reported DID is **not** accepted as a trust anchor. v2 is reserved for a future payload shape that carries the version inside the signed bytes; this client refuses to verify v2+ rather than guessing. |
| UCAN | Delegation token for future capability-based workflows. |
| UCAN | Capability token. An owner delegates `git/push` on a repo to another DID; the node honors it when the proof chain roots at that owner. See [`docs/RUN-A-NODE.md`](docs/RUN-A-NODE.md). |
| Peer announce | Node-to-node HTTP announcement of DID + public URL. |
| Gossipsub | libp2p topic for ref-update events. |
| Smart HTTP | Standard git protocol over HTTP for clone/fetch/push. |
Expand Down Expand Up @@ -401,8 +401,8 @@ Important node settings:
| `GITLAWB_P2P_BOOTSTRAP` | Comma-separated libp2p multiaddrs. |
| `GITLAWB_P2P_KEY` | Path to the node's persistent libp2p identity key file, which fixes the PeerId across restarts. Must name a key file inside a directory, such as `/data/keys/p2p.key` or `./keys/p2p.key`. Two kinds of problem are handled differently. A value that cannot name a securable key file at all (a bare filename, `..` traversal, a trailing `/`, the filesystem root, or a `~/` path that escapes home) is refused before the node binds, so the process exits. Anything wrong with the storage itself (a symlinked or non-directory parent, an unsafe ancestor or working directory, an unreadable or malformed key) leaves p2p off while HTTP keeps serving; the node logs it at error level as `p2p_identity_key_load_failed` and sets the `gitlawb_p2p_identity_key_load_failed` metric to 1. Note that `/health` still reports healthy in that state, so alert on the log event or the metric rather than on the health check. Correcting the storage and restarting restores the same PeerId. On Unix a new key is created `0600` inside a `0700` directory. A key directory that grants access beyond its owner is tightened to `0700` on start; one that is closed too far is refused with a `chmod 700` remedy rather than widened, since a directory locked down on purpose is an operator decision. Rotate the key only when the node logs that the directory was group- or world-writable (or the key file itself was group- or world-readable). An ordinary `0755` directory is tightened too, but others could not read a `0600` key or replace its directory entry. An existing key file is never modified: one with group or other bits set is rejected (`chmod 600`), and so is one this node cannot read. On other platforms no permissions are enforced. Default `~/.gitlawb/p2p.key`; point it at a persistent volume when running in a container. |
| `GITLAWB_BOOTSTRAP_DISABLE_SEEDS` | Disable embedded seed peers for isolated dev/test networks. |
| `GITLAWB_REQUIRE_SIGNED_PEER_WRITES` | Require peer writes to prove their DID on every transport: RFC 9421 signatures on the announce/sync routes, and an Ed25519 payload signature on gossip ref-update events. Defaults to `false` during the staged rollout below. Upgrade every gossip publisher before enabling, or their updates are dropped with no error on their side. |
| `GITLAWB_ENFORCE_OWNER_PUSH` | Require the authenticated pusher to be the repo owner on `git-receive-pack`. **Defaults to `true`.** A `did:key` signature is authentication, not authorization (anyone can mint a key and sign), so with this off every signed caller may push to every repository, private ones included. Delegated and CI keys count as non-owners: a UCAN `git/push` capability is verified but not yet honored for authorization, so they cannot push while this is on. Set `false` only for a rolling upgrade; see [`docs/RUN-A-NODE.md`](docs/RUN-A-NODE.md). |
| `GITLAWB_REQUIRE_SIGNED_PEER_WRITES` | Require signed peer announce/sync writes. Defaults to `false` during the staged rollout below. |
| `GITLAWB_ENFORCE_OWNER_PUSH` | Require the pusher on `git-receive-pack` to be the repo owner, or to present an owner-rooted, time-bounded UCAN `git/push` capability naming the repository (`X-Ucan`). **Defaults to `true`.** A `did:key` signature is authentication, not authorization — anyone can mint a key and sign — so with this off every signed caller may push to every repository, private ones included. A CI or delegated key with no such delegation counts as a non-owner and cannot push while this is on. The delegation clears the push gate only; on a private repository the delegate must also be a reader. Set `false` only for a rolling upgrade; see [`docs/RUN-A-NODE.md`](docs/RUN-A-NODE.md). |
| `GITLAWB_AUTO_SYNC` | Enable automatic sync from known peers. |
| `GITLAWB_RECONCILIATION_SWEEP` | Enable the hourly durability sweep that re-pins/backstops missing objects (default `true`; disabled when no IPFS/Pinata backend is configured). Public pin repair runs against any configured backend (IPFS, Pinata, or both). Encrypted recovery repair requires local IPFS (`GITLAWB_IPFS_API`); Pinata-only nodes reconcile public pins only. |
| `GITLAWB_MAX_PACK_BYTES` | Max git pack body size for smart-HTTP routes. |
Expand Down
9 changes: 9 additions & 0 deletions crates/git-remote-gitlawb/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,17 @@ path = "src/main.rs"
gitlawb-core = { path = "../gitlawb-core", features = ["redirect"] }
anyhow = { workspace = true }
reqwest = { workspace = true }
# Reading the node's DID from `GET /` so a delegated push can address its
# invocation to the right executor.
serde_json = { workspace = true }
# The invocation carries the delegation's expiry, so an unbounded write capability
# is never minted; converting the stored i64 timestamp needs chrono.
chrono = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
# Home-directory lookup for the shared GITLAWB_KEY resolver in gitlawb-core,
# which takes `home` as an argument so core's dependency allowlist stays lean.
dirs = "5"

[dev-dependencies]
mockito = "1"
Expand Down
Loading
Loading