mirror of
https://github.com/Twigpine/node.git
synced 2026-11-09 18:08:02 -10:00
fix/zeroize-seed-bytes
45
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9a46e8cbb7 |
fix(core): zeroize the derived X25519 secret (#65) (#66)
x25519_secret_from_seed derived the X25519 private scalar from the Ed25519 seed and returned it as a bare [u8; 32]. Because [u8; 32] is Copy with no Drop (and sha2's digest output likewise has no zeroize), the scalar, the SHA-512 digest, and the returned temporary were released without being scrubbed, leaving secret-derived material in freed memory. This is the same Copy-no-Drop class fixed for the raw seed in #41, on the derived secret. Return Zeroizing<[u8; 32]>, build the scalar directly into a zeroizing buffer so no bare secret local persists, and explicitly wipe the SHA-512 digest before it drops. Callers deref the result into crypto_box::SecretKey, which already scrubs its own copy. No behavior change: the derived scalar and all decryption output are byte-identical, covered by the existing ed25519_to_x25519_keypair_agrees and seal_open_round_trip_for_recipients tests. Stacked on #41 (#64). |
||
|
|
e4f5e87e0b |
fix(node): migrate node_seed pin caller to to_seed()
main grew a third seed_bytes() caller (repos.rs, from #40) after this branch was cut. Route it through to_seed() too: node_seed is moved into a long-lived tokio::spawn pin task, so the Zeroizing wrapper scrubs the copy when that task ends. encrypt_and_pin takes &[u8; 32]; the wrapper deref-coerces, so the call site is unchanged. |
||
|
|
38e654a289 | Merge remote-tracking branch 'origin/main' into fix/zeroize-seed-bytes | ||
|
|
4f814459cc |
test(core): pin to_seed's Zeroizing return type, drop duplicate round-trip
The to_seed_is_sole_seed_accessor test was named for a compile-time property its body couldn't assert, and its from_seed -> DID check duplicated from_seed_round_trip. Replace it with a type-annotated binding that fails to compile if to_seed ever returns a bare [u8; 32] again, plus the determinism check, which is the only behavior it uniquely covered. |
||
|
|
7533b74f79 |
ci: harden PR pipeline (audit, MSRV, beta matrix, release + docker smoke) (#57)
* ci: harden PR pipeline (audit, MSRV, release build, beta matrix, docker smoke) Restructure the single stable-only fmt+clippy+test job into separate jobs: fmt+clippy (stable), a test matrix on stable+beta (beta non-blocking), build --release, cargo audit, an MSRV check pinned to Rust 1.91, and a Docker build smoke test mirroring the release workflow. Add concurrency cancellation for superseded PR runs and per-job timeouts. Declare rust-version = "1.91" in [workspace.package] and inherit it in every member crate so the stated MSRV is a real, CI-enforced contract. * ci: disable credential persistence on checkout steps Set persist-credentials: false on all six actions/checkout steps. These jobs run untrusted PR code via cargo and docker build, and checkout's default leaves the GITHUB_TOKEN in the local git config where that code could read it. None of the jobs push or otherwise need git credentials. * fix(deps): bump rustls-webpki to 0.103.13 Clears three advisories flagged by the new cargo audit job: a reachable panic in CRL parsing and two name-constraint bypasses (wildcard and URI). Semver-compatible patch, lockfile only, no manifest or code change. cargo also corrects a stale lockfile reference while rewriting Cargo.lock: data-encoding-macro-internal 0.1.17 (a build-time macro crate) requires syn 1.x, and its dependency entry is re-pinned from syn 2.0.117 to the 1.0.109 it actually resolves to. Unrelated to the bump, unavoidable when cargo touches the lock, and the more-correct state. * ci(audit): suppress no-fix advisories with justification and a re-check The cargo audit job stays hard-fail. The remaining three advisories have no available upstream fix, so they are suppressed in .cargo/audit.toml (the location cargo-audit actually reads; a repo-root audit.toml is not read), each with an inline rationale and removal condition: - RUSTSEC-2026-0118/0119 (hickory-proto 0.25.2, via libp2p-dns 0.44): reachable, since the node wraps QUIC in the system DNS resolver. No fix until libp2p-dns ships hickory 0.26. Accepted, reachable risk. - RUSTSEC-2023-0071 (rsa Marvin): in the lockfile via sqlx's unconditional sqlx-mysql, but not linked (sqlx is postgres-only), so no RSA oracle exists; no upstream fix regardless. Two controls keep the suppression honest. The PR audit job echoes the active ignore list to the job summary, so a green check is not read as advisory-clean. A weekly scheduled job re-runs cargo audit without the ignores for visibility and hard-fails if hickory-proto moves off 0.25.x while the ignores remain, so the suppression cannot silently outlive the upstream fix. * ci(audit): reference tracking issue #76 in the hickory ignores * ci(audit): treat hickory-proto removal as drift too The scheduled drift guard only failed when hickory-proto was present and off 0.25.x, so removing it from Cargo.lock entirely left the RUSTSEC-2026-0118 /0119 ignores in place with the guard still passing. Treat an absent version as drift as well, so dead ignore entries are caught whether the dependency is bumped past 0.25.x or removed. |
||
|
|
abdc775770 |
feat(node): blind recipient identities at rest and gate B1 by repo readability (#40)
* feat(node): blind recipient identities at rest and gate B1 by repo readability
The origin no longer stores recipient DIDs. Migration v5 replaces the
encrypted_blobs.recipients column with an opaque, node-keyed recipients_tag
used only to detect a recipient-set change for re-seal. B1 discovery and fetch
are now gated by the same repo-readability check the git read path uses, not by
per-recipient matching; decryption is gated by the envelope crypto, so a
non-recipient who can read the repo sees a blob's {oid, cid} but cannot open it.
encrypt_and_pin keys the tag from the node seed and returns {oid, cid}; the
Arweave manifest tuple drops the now-unused recipient vec.
A DB compromise no longer reveals the reader set; recovering it would require
brute-forcing candidate DID sets against the keyed tag with the node key.
* chore: remove redundant .gitignore entry (covered by local exclude)
* fix(node): gate /replicate by repo readability; harden reseal on DB error
Address review on the at-rest blinding change:
- The encrypted-blobs/replicate listing returned {oid, cid} with no visibility
check, so a non-readable repo's blob index was reachable by an unauthenticated
caller who guessed {owner}/{repo}. Gate it by the same repo-readability check
discovery and fetch use. For the intended case (a public repo with withheld
subtrees) the public root keeps this open to peers; only fully non-readable
repos are withheld, which is the desired behavior.
- encrypt_and_pin treated a recipients_tag DB read error as a cache miss and
resealed, causing avoidable IPFS writes during a partial outage; skip and retry
on the next push instead.
- Correct the get_encrypted_blob doc comment to describe repo-readability access.
|
||
|
|
d45318281c |
fix(core): route seed access through the zeroizing wrapper (#41)
Remove Keypair::seed_bytes(), which returned a bare [u8; 32] copy of the Ed25519 private seed. Because [u8; 32] is Copy with no Drop, each caller's copy was released without being scrubbed, leaving secret key material in freed memory. The zeroizing accessor to_seed() -> Zeroizing<[u8; 32]> already existed beside it; this makes it the sole seed accessor and migrates the two in-crate callers (envelope open path and a crypto test) to it via deref coercion. No behavior change to envelope encryption: the derived X25519 secret is byte-identical, covered by the existing round-trip tests. Adds an identity test pinning to_seed() as the only seed accessor. |
||
|
|
e37ea7fec6 |
fix(node): enforce path-scoped visibility on the REST read API (#52)
* fix(node): enforce path-scoped visibility on the REST read API The repo-scoped REST read endpoints (blob, tree, commits, refs, changelog, pulls, pr diff, repo metadata) read straight from disk after a bare repo lookup, with no visibility check, so private repositories and private subtrees of public repositories were served to unauthenticated callers, bypassing the withholding the git and replication paths enforce. Add a shared authorize_repo_read gate (resolve repo, load rules, visibility_check, 404 on deny) and call it from every repo-scoped read handler. get_blob gates at the requested file path; get_pr_diff withholds the whole diff when it touches a path the caller cannot read. Attach optional_signature to the read routes so authorized readers authenticate. Listings stay gated at the repo root, consistent with mode B exposing tree and commit SHAs over git. Closes #51 * fix(node): gate PR reviews/comments and fork on repo read visibility The REST visibility gate added in this PR missed three read surfaces: list_reviews and list_comments served a private repo's PR reviews and comments to unauthenticated callers, and fork_repo cloned a full mirror of the source without checking the caller could read it. Route all three through authorize_repo_read at the repo root, matching the git-protocol gate. Also make branch_diff_names fail closed: it ignored git's exit status and returned an empty path list on error, which would let get_pr_diff serve a diff it should have withheld. Fix changelog.rs import order to satisfy cargo fmt. * fix(node): NUL-delimit branch_diff_names to keep path-gate enforcement exact git diff --name-only quotes and escapes paths containing newlines, so they no longer match the visibility globs in get_pr_diff. On a public repo where / is allowed but a subtree is denied, a touched restricted path could be missed by the deny check and the full PR diff disclosed. Use -z and split on NUL to preserve exact path bytes. |
||
|
|
197c4e1cf1 |
Add contributor templates, issue forms, and PR triage automation (#58)
* docs: add pull request template * docs: add structured issue forms and template chooser config * chore: add CODEOWNERS for CI, release, and security paths * ci: add quality-signal PR triage workflow * ci: add label-gated auto-stale workflow for ignored PRs * docs: document PR quality bar and triage/stale policy in CONTRIBUTING * ci: recognize inline #[cfg(test)] tests and fix triage comment grammar |
||
|
|
a5d2264cc0 |
ci(release): smoke-test packaged binaries' --version before attaching (#59)
The release-binaries job packaged gl, git-remote-gitlawb, and gitlawb-node into the release tarball but never ran them, so a broken artifact could ship. This is the follow-up promised in #30 and the gap noted in OSS-READINESS-AUDIT ("release binaries are not smoke-tested after packaging"), now unblocked by git-remote-gitlawb gaining --version. Run each binary's --version after build and assert the released version string is present. Skipped for aarch64-unknown-linux-musl, which is cross-compiled and can't execute on the x86_64 runner. Co-authored-by: FrozenRaspberry <[email protected]> Co-authored-by: Claude Opus 4.8 <[email protected]> |
||
|
|
5ff7af84fa |
feat: encrypted replication for private subtrees (B1/B2/B3) for #18 (#36)
* feat(core): ed25519 to x25519 key conversion for envelope crypto * feat(core): seal/open per-blob encryption envelopes * feat(node): resolve per-blob encryption recipients (least-privilege) * feat(node): encrypted_blobs table and recipient-scoped queries * feat(node): encrypt-then-pin withheld blobs at push (IPFS) At the push chokepoint, after pinning withheld objects, resolve each withheld blob's recipient DIDs and seal it to their Ed25519 keys with seal_blob, pinning the ciphertext to IPFS and recording it in encrypted_blobs. Best-effort per blob: failures are logged and skipped, never pinned in plaintext. Pinata replication is unchanged; B1 encrypts to IPFS only. Adds ed25519-dalek as a direct dependency of gitlawb-node (it was only declared in the workspace Cargo.toml). * feat(node): authenticated discovery and fetch for encrypted blobs * feat(gl): transparent recovery of authorized encrypted blobs on clone * style: cargo fmt * fix(node): re-seal encrypted blob when recipient set changes Skip re-pinning only when an existing envelope already covers exactly the current recipients. A reader added to a rule after the first pin now gets a re-seal on the next push instead of being permanently locked out. Reader removal stays non-retroactive (the old envelope is already public). * feat(node): unscoped encrypted-blob listing for replication * feat(node): encrypted-blobs/replicate endpoint for peer mirrors * feat(node): route encrypted-blobs/replicate under git_read_routes * feat(node): peer mirrors replicate encrypted withheld blobs (Option B2) * feat(node): anchor_encrypted_manifest for Option B3 Arweave manifests * feat(node): encrypt_and_pin returns the per-push sealed delta * feat(node): anchor encrypted-blob manifest on push (Option B3) * feat(gl): Arweave/IPFS gateway recovery for encrypted blobs (Option B3) * feat(gl): transparent Arweave fallback recovery on clone (Option B3) * style: cargo fmt * fix(gl): bound Arweave recovery gateway requests with a 30s timeout * test(gl): read-path recovery tests for Arweave fallback; quiet promisor present-check Add two hermetic integration tests for recover_from_arweave that drive the full read path over mocked Arweave GraphQL + IPFS gateways: discover the manifest, fetch it, fetch the envelope, decrypt, and install the withheld blob. One covers an authorized recipient (blob installed), the other a non-recipient (nothing recovered). Both simulate origin death by removing the promisor remote and enable uploadpack.allowFilter so the blob is truly withheld over file://. Also harden the local presence check in recover_from_arweave with GIT_NO_LAZY_FETCH=1 and .output() so the expected 'missing object' case does not trigger a wasted promisor fetch or leak git stderr to the user. * fix: surface corrupt recipients JSON and silent recovery I/O failures db: parse_recipients now propagates a descriptive error instead of defaulting corrupt recipients JSON to an empty list, which would have denied authorized readers and handed peers incomplete metadata. gl: clone recovery now warns when the sparse-checkout file cannot be read or written, or when the post-recovery checkout fails, instead of silently discarding those errors and claiming files were recovered. * feat(core): blind recipient identities in withheld-blob envelopes Envelope v2 drops the cleartext recipient public key (kid) from each wrapped-key header entry, so a party holding the public IPFS/Arweave copy can no longer enumerate who is authorized to decrypt a withheld blob. The version is bumped to 2 and v1 envelopes are rejected. open_blob now selects the reader's entry by trial decryption (the AEAD tag authenticates exactly one entry) instead of matching on the public key. Recipient count is still visible; identity is not. Scope is envelope-only, the node DB recipients column and peer replication metadata are unchanged. Co-authored-by: CommandCodeBot <[email protected]> * fix(core): reject malformed envelope nonces instead of panicking open_blob fed attacker-controlled envelopes to GenericArray::from_slice and XNonce::from_slice, which panic on a wrong-length input. Validate both the per-recipient box nonce and the body nonce to 24 bytes before use: skip a recipient entry whose nonce is malformed, and return an error for a malformed body nonce, so the public recovery path surfaces an error rather than panicking. * feat(node): blind recipient identities on the B2 replication surface The encrypted-blobs/replicate endpoint shipped every blob's full recipient DID list to mirrors, which persisted it, so any mirroring peer learned the reader set. The v2 envelope blinding already removed recipient public keys from the pinned envelopes, so the comment justifying this (DIDs are already public) was no longer true. /replicate now returns {oid, cid} only. Mirrors detect a re-seal by the CID changing (the OID is stable across re-seals) instead of comparing recipient sets, and store no recipient identities. Origin-side authz and the at-rest recipients column are unchanged; this blinds only the peer-facing surface. * feat(node): stop anchoring recipient identities to Arweave The B3 encrypted-blob manifest anchored {oid, cid, recipients} to Arweave, a permanent public record of every blob's reader set. The v2 envelope blinding already removed recipient keys from the pinned envelopes, so the comment justifying this (recipient DIDs are already public) was no longer true. The manifest now anchors {oid, cid} only. The gl reader already ignores the recipients field (it recovers by trial decryption), so no reader changes. --------- Co-authored-by: CommandCodeBot <[email protected]> |
||
|
|
8680d0f9d6 |
feat(node): replication enforcement (Phase 2) for #18 (#34)
* docs(node): Phase 3 subtree-withholding plan with Task 0 spike findings * feat(node): resolve withheld blob OIDs for path-scoped visibility * feat(node): filtered upload-pack serve that omits withheld blobs * feat(node): serve filtered pack when caller has withheld subtree blobs * test(node): end-to-end assert served pack omits withheld blob * docs(node): note why info/refs is not gated on subtree visibility * fix(node): frame filtered serve as protocol v0 and prove it with a real clone upload_pack_excluding emitted a v2 packfile section, but info_refs advertises v0, so real clients negotiated v0 and rejected the response with 'expected ACK/NAK, got packfile'. Frame the v0 stateless-rpc shape instead (NAK, then the pack via side-band-64k when offered). Add an end-to-end test that stands up info_refs + upload_pack_excluding and runs a real git partial clone, asserting the withheld blob's bytes never reach the client while its tree entry and SHA stay visible. A stock full clone cannot consume the pack (it is not closed under reachability, so fetch fails the connectivity check); a partial clone is required. * docs(node): correct Phase 3 caveat (full clone refused at fetch, not checkout) * test(node): prove incremental fetch still withholds; document negotiation choice Add a real-git test that partial-clones, pushes a new commit server-side, then fetches: the new object arrives and the withheld blob stays absent. This pins down that ignoring have/want negotiation (always sending a self-contained pack of all refs minus withheld, with NAK) is correct for both clone and fetch; the only cost is a fetch re-sends the full object set. Refactor the real-git tests onto a shared server harness and document the negotiation decision in code and in the plan's follow-ups. * refactor(node): address CodeRabbit review on PR #28 Move the two blocking git shell-outs in the filtered upload-pack path off the async worker thread, matching the tokio::process / spawn_blocking usage already in this file: build_filtered_pack (rev-list + pack-objects) and withheld_blob_oids (per-ref ls-tree) now run inside spawn_blocking so a large repo cannot stall the tokio runtime. Behavior is unchanged. Also fix the Task 0 findings block in the Phase 3 plan: it still recorded v2 packfile framing, which is the exact path that failed against a real client and was corrected to v0. The block now documents the shipped v0 contract. Drop a stray trailing code fence flagged by markdownlint (MD040). The speculative ls-tree timeout and the public/no-rules fast-path from the review are intentionally left out: the timeout guards against adversarial repos we do not yet host, and the fast-path is a micro-optimization not worth the extra branch right now. * chore(node): drop planning doc from PR per maintainer request kevincodex1 asked to keep the superpowers planning docs out of the repo. The Phase 3 plan was scaffolding for this change, not something the project needs to carry. Removing it leaves only the code and tests in the PR. * chore: gitignore local planning docs (docs/superpowers/) * feat(node): replicable_objects filter for replication enforcement * test(node): pin anonymous-caller contract of withheld_blob_oids * feat(node): IPFS pinning skips withheld blob OIDs * feat(node): Pinata pinning skips withheld blob OIDs * feat(node): enforce visibility on push replication (IPFS/Pinata/gossip/Arweave) * test(node): announce gate matches anonymous repo readability * fix(node): run withheld_blob_oids off the async worker on push The receive-pack replication chokepoint called withheld_blob_oids directly on the tokio worker, where its blocking git ls-tree walk can stall the runtime for repos with many refs. Wrap it in spawn_blocking to match the upload-pack serve path. --------- Co-authored-by: Kevin Codex <[email protected]> |
||
|
|
c03c9af8fa |
feat(sync): auto-register as replica with origin after successful mirror (#56)
The sync worker mirrored repos from peers but never told the origin, so
replica_count (repo_replicas table, GET /api/v1/repos/:owner/:repo/replicas)
stayed at 0 network-wide — the dashboard showed 0 replication even for
repos that were actually mirrored.
After each successful clone/fetch the worker now sends a signed
PUT /api/v1/repos/{owner}/{repo}/replicas to the origin with our
public_url, using the same HTTP-Signature scheme as push notifies.
Best-effort by design: skipped when public_url is unset, failures are
logged without failing the sync, and the PUT is idempotent on the origin
(201 first, 200 after) so re-registering every sync is self-healing.
Nodes need this build before their mirrors are counted: a new node's
repos show replication once its peers upgrade.
Co-authored-by: OpenClaude <[email protected]>
|
||
|
|
7a8b6c4974 |
Aws ecr image (#55)
* feat(infra): optional private ECR repo for the node image create_ecr_repo = true provisions an ECR repository in-account and switches the instance to pull from it: the instance role gets AmazonEC2ContainerRegistryReadOnly and user-data configures amazon-ecr-credential-helper, so first-boot pulls and SSM upgrades authenticate via the role (no expiring login tokens). README documents the local build-and-push flow. Motivation: ghcr.io/gitlawb/node is not anonymously pullable (403), which left first boot unable to start the stack. Co-Authored-By: OpenClaude <[email protected]> * feat(infra): HTTPS via Caddy + production tier (RDS, alarms, S3 state) - domain_name: Caddy sidecar terminates TLS on 80/443 with auto-renewed Let's Encrypt certs (persisted on the data volume); public_url defaults to https://<domain>; SG opens 80/443 — matches the Fly nodes - use_rds: managed Postgres (db.t4g.micro, encrypted, 7-day PITR, deletion protection) replaces the in-container postgres; DB security group only admits the node instance - alert_email: SNS email alerts wired to CloudWatch alarms — external HTTPS /health via Route53 health check, EC2 status checks, CPU, RDS free storage - user-data grows the filesystem when the EBS volume is enlarged - terraform state moved to S3 (versioned, KMS-encrypted, public-blocked bucket) with lockfile locking (requires terraform >= 1.10) Co-Authored-By: OpenClaude <[email protected]> --------- Co-authored-by: OpenClaude <[email protected]> |
||
|
|
7a99d0f27c |
fix(node): make Tigris repo hydration resilient to corrupt archives & failed writes (#54)
Repo pushes 500'd whenever a repo's Tigris archive was unreadable (corrupt or truncated) or the node was under disk pressure, because hydration was treated as fatal and a failed extraction could clobber the good local copy. Three hardening changes: - tigris: extract archives atomically (unpack into a temp dir, swap into place only on success) so a corrupt/truncated archive can never clobber a good existing local copy. - repo_store: acquire_fresh / acquire_write fall back to the valid local copy when a Tigris download fails, instead of hard-failing the operation; the post-write upload then re-syncs (self-heals) Tigris. - repo_store: RepoWriteGuard::release(success) uploads to Tigris only when the write succeeded, so a half-applied / inconsistent repo is never propagated to Tigris or to other nodes. Co-authored-by: OpenClaude <[email protected]> |
||
|
|
c7df6cad20 |
Fix/backport prod fixes (#39)
* fix: backport production fixes from monorepo Three fixes that landed on the production node but never made it here: - trigger_sync: 30s timeout with the body read inside it (was 5s covering headers only — large unpaginated repo lists from canonical nodes and transpacific peers aborted mid-body), plus distinct logging for non-2xx vs timeout - announce: reject self-announcements by public URL or own DID; complements the existing boot-time prune_self_peers so self-loop rows never enter the peers table in the first place - list_all_repos_paged: report MAX(updated_at) across each logical repo group instead of the canonical row's own value — gossip pushes touch only the mirror row, so canonical timestamps go stale Monorepo refs: 8c2fe1b, c1dc33e, ef21c75. Co-Authored-By: Claude Fable 5 <[email protected]> * fix(p2p): dual-stack QUIC listen + DNS multiaddr dialing for Fly 6PN Backports the monorepo TCP-era p2p fixes (9f875b6, 267d371) adapted to the QUIC transport: - listen on /ip6/:: as well as /ip4/0.0.0.0 — Fly's 6PN inter-app network is IPv6-only (<app>.internal resolves to AAAA records), so without an IPv6 socket peers on the private network can't reach us - wrap the QUIC transport in libp2p-dns so bootstrap multiaddrs can use /dns6/<app>.internal — dialing peers through the public anycast edge breaks the handshake when the proxy closes the connection mid-stream Co-Authored-By: Claude Fable 5 <[email protected]> * fix(sync): HTTP notify records received_ref_updates like the gossip path A push propagated via the /api/v1/sync/notify HTTP fallback replicated the repo but left no trace in received_ref_updates — only the libp2p gossip path recorded it, so /api/v1/events/ref-updates went blind whenever the mesh wasn't delivering. Insert the same record from the notify handler, with optional provenance fields (pusher_did, old_sha, timestamp, cert_id) that new senders already include. Monorepo ref: 087eeac. Co-Authored-By: Claude Fable 5 <[email protected]> --------- Co-authored-by: Claude Fable 5 <[email protected]> |
||
|
|
b61a1bd46f |
fix(api): blob endpoint returns 400/404 instead of 500 on bad paths (#37)
Unnormalized "../.." paths from crawler-exploded relative links made `git show` fail and surfaced as 500s. Reject traversal segments with 400 up front, and map paths absent from the tree to 404 so only real git failures count as server errors. Co-authored-by: Claude Fable 5 <[email protected]> |
||
|
|
e365a57b51 |
feat(node): peer partial-mirrors for repos with private subtrees (#35)
* feat(node): withheld-paths endpoint reporting a caller's denied globs * feat(gl): gl clone with promisor + sparse-exclude for private subtrees * test(gl): gl clone repo-argument parsing * fix(visibility): gate withheld-paths and handle nested/exact globs Three fixes from the PR #33 review: - withheld_paths now applies the whole-repo "/" read gate (returns repo-not-found when the caller cannot read the root), matching the git read endpoints. Without it the endpoint disclosed a private repo's existence and path layout to unauthorized callers. The withheld_globs doc already assumed this gate existed; now it does. - A nested allow under a denied parent (e.g. "/secret/public/**" allowed, "/secret/**" denied) was over-withheld: the client sparse-excluded the whole parent and hid paths the caller may read. The endpoint now also returns a "reinclude" list (allowed globs strictly under a denied one) and gl clone re-includes them in the sparse spec after the excludes. - Wildcard-free globs like "/docs/private" match both the exact path and a subtree (per glob_matches), but the client only emitted the subtree exclude. sparse_patterns now emits both "/docs/private" and "/docs/private/". Verified the exclude-then-reinclude sparse ordering checks out cleanly with real git, plus unit tests for reincluded_globs, the nested re-include, the exact-path exclude, and sparse_patterns. * fix(gl): reject multi-slash repo input and stop failing open on withheld-path errors split_once('/') accepted owner/name/extra, smuggling a path segment into the repo name that then flowed into the API path and clone URL; reject it. fetch_withheld swallowed every network/auth/5xx/JSON error into an empty result, dropping to a stock clone that the node refuses once blobs are withheld. Now only 404/501 (endpoint unsupported) fall back to empty; the rest propagate so the real cause surfaces. * test(gl): guard three-level nested visibility in sparse clone Add a regression test for deny /secret, allow /secret/public, deny /secret/public/admin and clarify the sparse-checkout comment. git does not re-traverse an explicitly excluded directory, so emitting all excludes before re-includes keeps the deepest deny in force; the broader parent re-include does not resurrect it. * fix(gl): robust default-branch detection and strict withheld-paths schema Read the default branch from the local origin/HEAD symref clone already set, instead of parsing the localized, network-dependent output of git remote show origin. Deserialize the withheld-paths body into a typed struct so a missing or mistyped withheld/reinclude field is a hard error rather than silently becoming an empty list, which would mask a server regression behind a later clone failure. * feat(node): classify mirror mode from origin withheld-paths * feat(node): promisor-aware mirror clone for withheld repos * feat(node): promisor-aware mirror fetch via origin remote * feat(node): classify and mirror peers per withheld-paths * style: cargo fmt * fix(node): bound sync withheld lookup and rehydrate on promisor->plain Give the sync worker's HTTP client a 30s timeout so a stalled peer withheld-paths lookup cannot hang the worker loop. When a repo that was mirrored as a promisor (mode B) becomes fully public, fetch_repo now clears remote.origin.promisor and partialclonefilter and refetches, so the once-withheld blobs are backfilled instead of the mirror staying permanently partial. |
||
|
|
4a59056a90 |
gl clone: clean partial clone of repos with private subtrees (#33)
* feat(node): withheld-paths endpoint reporting a caller's denied globs * feat(gl): gl clone with promisor + sparse-exclude for private subtrees * test(gl): gl clone repo-argument parsing * fix(visibility): gate withheld-paths and handle nested/exact globs Three fixes from the PR #33 review: - withheld_paths now applies the whole-repo "/" read gate (returns repo-not-found when the caller cannot read the root), matching the git read endpoints. Without it the endpoint disclosed a private repo's existence and path layout to unauthorized callers. The withheld_globs doc already assumed this gate existed; now it does. - A nested allow under a denied parent (e.g. "/secret/public/**" allowed, "/secret/**" denied) was over-withheld: the client sparse-excluded the whole parent and hid paths the caller may read. The endpoint now also returns a "reinclude" list (allowed globs strictly under a denied one) and gl clone re-includes them in the sparse spec after the excludes. - Wildcard-free globs like "/docs/private" match both the exact path and a subtree (per glob_matches), but the client only emitted the subtree exclude. sparse_patterns now emits both "/docs/private" and "/docs/private/". Verified the exclude-then-reinclude sparse ordering checks out cleanly with real git, plus unit tests for reincluded_globs, the nested re-include, the exact-path exclude, and sparse_patterns. * fix(gl): reject multi-slash repo input and stop failing open on withheld-path errors split_once('/') accepted owner/name/extra, smuggling a path segment into the repo name that then flowed into the API path and clone URL; reject it. fetch_withheld swallowed every network/auth/5xx/JSON error into an empty result, dropping to a stock clone that the node refuses once blobs are withheld. Now only 404/501 (endpoint unsupported) fall back to empty; the rest propagate so the real cause surfaces. * test(gl): guard three-level nested visibility in sparse clone Add a regression test for deny /secret, allow /secret/public, deny /secret/public/admin and clarify the sparse-checkout comment. git does not re-traverse an explicitly excluded directory, so emitting all excludes before re-includes keeps the deepest deny in force; the broader parent re-include does not resurrect it. * fix(gl): robust default-branch detection and strict withheld-paths schema Read the default branch from the local origin/HEAD symref clone already set, instead of parsing the localized, network-dependent output of git remote show origin. Deserialize the withheld-paths body into a typed struct so a missing or mistyped withheld/reinclude field is a hard error rather than silently becoming an empty list, which would mask a server regression behind a later clone failure. |
||
|
|
3a401eb079 |
feat(git-remote-gitlawb): add --version and --help flags (#30)
git-remote-gitlawb was the only workspace binary without `--version`, so
release smoke tests could not validate the packaged helper directly. This
is called out in docs/MAINTAINER-ROADMAP.md ("Add --version support to
git-remote-gitlawb so release smoke tests can validate it directly") and
docs/OSS-READINESS-AUDIT.md.
- `--version`/`-V` prints `git-remote-gitlawb <version>`, matching the
`<bin> <version>` format the clap-based `gl` and `gitlawb-node` emit so
release tooling can treat all three binaries uniformly. The version
comes from CARGO_PKG_VERSION.
- `--help`/`-h` prints concise usage (URL form, env vars, flags).
- Flags are only recognized as the first argument, so git's
`<remote-name> <url>` remote-helper invocation path is untouched; a
remote literally named "--version" is not mistaken for the flag.
- Informational flags are handled before tracing init so output stays
clean for scripts.
Adds unit tests for argument classification (including the no-args and
misplaced-flag edge cases) and the version/help strings.
Co-authored-by: FrozenRaspberry <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
|
||
|
|
61b383019f |
feat(node): subtree content withholding (Phase 3) for #18 (#28)
* docs(node): Phase 3 subtree-withholding plan with Task 0 spike findings * feat(node): resolve withheld blob OIDs for path-scoped visibility * feat(node): filtered upload-pack serve that omits withheld blobs * feat(node): serve filtered pack when caller has withheld subtree blobs * test(node): end-to-end assert served pack omits withheld blob * docs(node): note why info/refs is not gated on subtree visibility * fix(node): frame filtered serve as protocol v0 and prove it with a real clone upload_pack_excluding emitted a v2 packfile section, but info_refs advertises v0, so real clients negotiated v0 and rejected the response with 'expected ACK/NAK, got packfile'. Frame the v0 stateless-rpc shape instead (NAK, then the pack via side-band-64k when offered). Add an end-to-end test that stands up info_refs + upload_pack_excluding and runs a real git partial clone, asserting the withheld blob's bytes never reach the client while its tree entry and SHA stay visible. A stock full clone cannot consume the pack (it is not closed under reachability, so fetch fails the connectivity check); a partial clone is required. * docs(node): correct Phase 3 caveat (full clone refused at fetch, not checkout) * test(node): prove incremental fetch still withholds; document negotiation choice Add a real-git test that partial-clones, pushes a new commit server-side, then fetches: the new object arrives and the withheld blob stays absent. This pins down that ignoring have/want negotiation (always sending a self-contained pack of all refs minus withheld, with NAK) is correct for both clone and fetch; the only cost is a fetch re-sends the full object set. Refactor the real-git tests onto a shared server harness and document the negotiation decision in code and in the plan's follow-ups. * refactor(node): address CodeRabbit review on PR #28 Move the two blocking git shell-outs in the filtered upload-pack path off the async worker thread, matching the tokio::process / spawn_blocking usage already in this file: build_filtered_pack (rev-list + pack-objects) and withheld_blob_oids (per-ref ls-tree) now run inside spawn_blocking so a large repo cannot stall the tokio runtime. Behavior is unchanged. Also fix the Task 0 findings block in the Phase 3 plan: it still recorded v2 packfile framing, which is the exact path that failed against a real client and was corrected to v0. The block now documents the shipped v0 contract. Drop a stray trailing code fence flagged by markdownlint (MD040). The speculative ls-tree timeout and the public/no-rules fast-path from the review are intentionally left out: the timeout guards against adversarial repos we do not yet host, and the fast-path is a micro-optimization not worth the extra branch right now. * chore(node): drop planning doc from PR per maintainer request kevincodex1 asked to keep the superpowers planning docs out of the repo. The Phase 3 plan was scaffolding for this change, not something the project needs to carry. Removing it leaves only the code and tests in the PR. * chore: gitignore local planning docs (docs/superpowers/) |
||
|
|
a2217bff27 |
fix(infra): drop fly idle_timeout 600 -> 120 (#38)
600s idle windows let hung clients pin Fly proxy connection slots up to hard_limit, which amplified the 2026-06-12 production outage. 120s was deployed there as the fix; carry it into the reference config. Co-authored-by: Claude Fable 5 <[email protected]> |
||
|
|
6abaf1d7ed |
feat: path-scoped repository visibility (Phase 1) for #18 (#25)
* feat: path-scoped repository visibility (Phase 1) Implements per-path read visibility for repositories, addressing #18. A repo's private status becomes a property of a path subtree rather than a whole-repo boolean, enforced on the git read path via the node's existing DID identity and RFC 9421 HTTP signatures. Phase 1 scope (whole-repo enforcement, data model for the rest): - visibility_rules data model (path_glob, mode, reader_dids) + DB access - visibility_check pure decision function (owner-always-allow, most- specific rule wins, reader-DID allow-list, is_public fallback) - enforcement on git_info_refs and git_upload_pack: unauthorized reads return 404 byte-identical to a missing repo (no existence leak) - owner-only management API: PUT/DELETE/GET /api/v1/repos/{o}/{r}/visibility - gl visibility set/remove/list CLI mode A (hide) is whole-repo ("/") only by construction: hiding a subtree's existence would rewrite ancestor tree hashes and diverge history. Subtrees use mode B (content withheld, SHAs intact); B-subtree clone filtering is deferred to a later phase and the CLI/API say so. 260 tests (69 node + 191 gl); fmt + clippy clean. * fix: address review on path-scoped visibility - migrations: move visibility_rules DDL out of the v1 migration into a new v3 (CodeRabbit, critical). Appending to v1 meant nodes that had already recorded v1 would skip the statements forever, so the table would be missing after upgrade. v3 matches the convention #23 set with v2 (agent_profiles) and the documented 'never append to v1' rule. - api: validate path_glob on set (must start with '/', no trailing '/', only a trailing '/**' wildcard, '/' for whole-repo not '/**'). Empty or slash-less globs would otherwise silently misconfigure access. Added unit tests for the accepted/rejected forms. 281 -> 283 tests pass (88 node + 195 gl); fmt + clippy clean. |
||
|
|
9e6e832ec5 |
Infra reorg (#27)
* refactor: move fly.toml into infra/fly/ Deployment configs now live under infra/, organized per target. Dockerfiles and docker-compose.yml intentionally stay at the repo root (shared by CI, build scripts, and the macOS app). Deploy with: fly deploy -c infra/fly/fly.toml Co-Authored-By: OpenClaude <[email protected]> * feat(infra): Terraform for single-EC2 AWS deployment infra/aws/ provisions a production-leaning single-node deployment: - t4g.small (AL2023 arm64) running the published ghcr.io/gitlawb/node image + postgres:16 via docker compose - separate encrypted EBS data volume (prevent_destroy) with daily DLM snapshots; survives instance replacement - postgres password via random_password -> SSM SecureString, fetched at boot by instance profile (never in user-data); optional secrets (operator key, Pinata JWT, S3 secret) follow the same path - SSM Session Manager access (no SSH by default), IMDSv2 required, metrics port closed unless explicitly opened - SSM command document for image upgrades (user-data runs once) Verified: terraform fmt/init/validate clean; rendered compose passes docker compose config; rendered user-data passes bash -n. Co-Authored-By: OpenClaude <[email protected]> * fix(infra): address CodeRabbit review feedback - api_url output now honors the public_url override (local.public_url) - disable xtrace around SSM secret fetch + .env write so secret values never reach the bootstrap log - add retry/timeout flags to the docker compose download - validate snapshot_retain_count (1-1000) at plan time - add language tags to fenced tree blocks (MD040) Co-Authored-By: OpenClaude <[email protected]> * fix(infra): address PR #27 review feedback - security group now derives its VPC from the selected subnet, so subnet_id overrides into non-default VPCs work - s3_access_key_id moves to SSM SecureString (same path as the secret key) instead of being embedded in user-data and state - ignore user_data drift on the instance: it only runs at first boot, so re-rendering caused pointless stop/starts; README documents the -replace workflow for config changes - DLM snapshot targeting is stack-specific (Snapshot + Name tags), not any Snapshot=true volume in the account - comment in fly.toml that it targets the shared test instance - user-data compose up gains --remove-orphans (consistent with the upgrade SSM document) - node service gets an HTTP /health healthcheck in compose Co-Authored-By: OpenClaude <[email protected]> * feat(infra): optional customer-managed KMS key for SSM secrets Add ssm_kms_key_id (default null = AWS-managed aws/ssm key). When set, all five SecureString parameters are encrypted with the CMK and the instance role gains kms:Decrypt scoped to that key — without it the boot-time secret fetch would fail. Co-Authored-By: OpenClaude <[email protected]> * fix(infra): scope CMK kms:Decrypt to this stack's SSM parameters The grant on the customer-managed key was unconditioned, allowing the instance role to decrypt any ciphertext under the same CMK. Restrict it with kms:ViaService = ssm.<region>.amazonaws.com and kms:EncryptionContext:PARAMETER_ARN limited to the stack's parameter ARNs. Co-Authored-By: OpenClaude <[email protected]> --------- Co-authored-by: OpenClaude <[email protected]> |
||
|
|
09a339745e |
feat: agent profiles (display name, bio, avatar, social links) (#23)
* feat: agent profiles — display name, bio, avatar, social links
Adds a profile system so agents and users can set metadata that the
frontend can render on profile pages.
CLI:
gl profile set --name "Axiom" --bio "AI builder" --twitter AxiomBot
gl profile show
gl profile get <did>
gl profile import profile.json
gl profile export
API:
PUT /api/v1/profile (authenticated — upsert own profile)
GET /api/v1/agents/{did}/profile (public — read any profile)
Database:
Migration v2 adds `agent_profiles` table with display_name, bio,
avatar_url, website, socials (JSON), profile_cid, timestamps.
Profile updates merge fields — setting --bio doesn't clear --name.
Bio capped at 280 chars, display_name at 50. Social links stored as
JSON (twitter, github, farcaster, telegram). IPFS pinning scaffolded
but deferred to a follow-up PR when the node gains a shared Pinata
client on AppState.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: apply cargo fmt to pass CI formatting check
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix: suppress dead_code warnings for pin_to_ipfs field and set_profile_cid method
Both are placeholder implementations for planned IPFS pinning — not yet wired
into the handler but intentionally part of the public API surface.
Co-Authored-By: Axiom Bot <[email protected]>
* feat: wire up IPFS pinning for agent profiles via Pinata
When pin_to_ipfs: true is sent in the profile update request and the
node has GITLAWB_PINATA_JWT configured, the profile JSON is pinned to
IPFS via Pinata and the CID is stored in the database.
- Removed #[allow(dead_code)] from pin_to_ipfs field and set_profile_cid
- Profiles serialize to JSON and pin via existing pinata::pin_object
- Graceful fallback: if Pinata JWT is empty or pin fails, profile is
saved normally without CID
- CID returned in response when available
---------
Co-authored-by: Axiom Bot <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
|
||
|
|
2ce4da9cc5 |
feat(node): graceful shutdown + Prometheus metrics endpoint (#22)
* feat(db): versioned schema migrations with idempotent backfill Replace the inline &str SQL array that re-runs on every node startup with a versioned migration system. Each migration is recorded in a new schema_migrations table and applied at most once per node, inside a single transaction. For nodes upgrading from a pre-migration-versioning build, the canonical `repos` table is used as a signal to mark v1 as already applied without re-running its ~140 statements — so existing operators see zero behavior change on first restart after this commit. Closes the gap called out in docs/OSS-READINESS-AUDIT.md:78. - Adds Db::migration_status() returning applied (version, name, applied_at) - Adds 6 unit tests validating the static migration catalogue (versions strictly increasing, names distinct, bodies non-empty, v1 name locked to initial_schema) - cargo fmt + clippy -D warnings + cargo test --workspace all clean * fix(db): make legacy migration backfill safe and lock-guarded Address two robustness gaps in the v1 migration runner on the live network: - Remove the "repos table present => v1 complete" backfill short-circuit. It assumed every existing node already had the full current schema; a node that was behind would be marked v1-applied while still missing newer tables or columns. Every v1 statement is idempotent (CREATE TABLE/INDEX IF NOT EXISTS, ADD COLUMN IF NOT EXISTS), so legacy installs now simply run v1 once: existing objects are no-ops, missing ones get created. - Guard the whole runner with a Postgres advisory lock so two instances against the same database (blue/green or rolling deploy) can't race to apply the same migration and trip the schema_migrations primary key. The lock is released explicitly, and automatically if the connection drops. Also document that per-migration transactions preclude CREATE INDEX CONCURRENTLY in future migrations. Co-Authored-By: Claude Opus 4.8 <[email protected]> * feat(node): graceful shutdown + Prometheus metrics endpoint Two operational-safety changes that the live network needs: 1) Graceful shutdown. A process-wide tokio::sync::watch::Sender<bool> in AppState is flipped on SIGINT (all platforms) or SIGTERM (Unix). Every long-lived loop now selects on the shutdown arm: * axum server (with_graceful_shutdown — drains in-flight) * libp2p swarm (drops Swarm, closes connections) * gossip task (exits between peer announces) * sync worker (exits between batches) * operator heartbeat loop * rate-limit cleanup loop * peer-count metrics poller * the deferred DID-record publish task New GITLAWB_SHUTDOWN_GRACE_SECS (default 30s) controls how long axum waits for in-flight requests before returning. 2) Prometheus /metrics endpoint. New metrics module exposes: * gitlawb_info{version, did} (constant gauge = 1) * gitlawb_pushes_total{repo} (counter) * gitlawb_fetches_total{repo} (counter) * gitlawb_auth_successes_total{route} (counter, helper) * gitlawb_auth_failures_total{route,reason} (counter, helper) * gitlawb_sync_queue_processed_total{status} (counter) * gitlawb_webhook_deliveries_total{result} (counter) * gitlawb_pack_size_bytes (histogram) * gitlawb_peers_connected (gauge) Opt-in via GITLAWB_METRICS_ADDR (e.g. 127.0.0.1:9091). The endpoint is on a separate listener so the public port stays unexposed; bind to localhost or a private interface. 3) Metric instrumentation hooks in api/repos.rs (push/fetch), webhooks.rs (delivery outcomes), and sync.rs (per-item status). The auth helpers are defined but not yet wired — that's a focused follow-up. Tests: * 3 new metrics::tests::* — encode returns valid prometheus text, record_* helpers are no-ops before init(), encode() errors if init() was never called. * cargo fmt + clippy -D warnings + cargo test --workspace all clean. Closes the metrics and graceful-shutdown gaps in docs/OSS-READINESS-AUDIT.md:131 and docs/MAINTAINER-ROADMAP.md:35. --------- Co-authored-by: Claude Opus 4.8 <[email protected]> |
||
|
|
927e4d0cfb |
feat(db): versioned schema migrations with idempotent backfill (#21)
* feat(db): versioned schema migrations with idempotent backfill Replace the inline &str SQL array that re-runs on every node startup with a versioned migration system. Each migration is recorded in a new schema_migrations table and applied at most once per node, inside a single transaction. For nodes upgrading from a pre-migration-versioning build, the canonical `repos` table is used as a signal to mark v1 as already applied without re-running its ~140 statements — so existing operators see zero behavior change on first restart after this commit. Closes the gap called out in docs/OSS-READINESS-AUDIT.md:78. - Adds Db::migration_status() returning applied (version, name, applied_at) - Adds 6 unit tests validating the static migration catalogue (versions strictly increasing, names distinct, bodies non-empty, v1 name locked to initial_schema) - cargo fmt + clippy -D warnings + cargo test --workspace all clean * fix(db): make legacy migration backfill safe and lock-guarded Address two robustness gaps in the v1 migration runner on the live network: - Remove the "repos table present => v1 complete" backfill short-circuit. It assumed every existing node already had the full current schema; a node that was behind would be marked v1-applied while still missing newer tables or columns. Every v1 statement is idempotent (CREATE TABLE/INDEX IF NOT EXISTS, ADD COLUMN IF NOT EXISTS), so legacy installs now simply run v1 once: existing objects are no-ops, missing ones get created. - Guard the whole runner with a Postgres advisory lock so two instances against the same database (blue/green or rolling deploy) can't race to apply the same migration and trip the schema_migrations primary key. The lock is released explicitly, and automatically if the connection drops. Also document that per-migration transactions preclude CREATE INDEX CONCURRENTLY in future migrations. Co-Authored-By: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: Claude Opus 4.8 <[email protected]> |
||
|
|
924bccd8e5 |
feat(gitlawb-attest): External Attestation v1 for ref-update certs (#20)
Adds an optional `attestations` field on ref-update certs plus a small
crate to sign and verify them. An envelope with no attestations
serializes to identical bytes as a bare cert, so nothing on the wire
changes for nodes that don't opt in.
Each attestation carries a `type` discriminator (`covenant/exec/v1`,
`slsa/v1.0`, `sigstore/dsse/v1`, ...), an opaque payload, a `cert_hash`
that binds it to one specific cert, a `did:key` signer, and a base64url
ed25519 signature over a domain-separated JCS-encoded input.
## Wire shape
cert_hash = SHA-256(JCS(cert_body \ {signatures, attestations}))
encoded as lowercase hex.
signing_input = b"gitlawb-attest-sig/v1\n"
|| JCS({type, payload, cert_hash})
sig = base64url-no-pad(Ed25519(signing_input))
The cert hash is reproducible across implementations because JCS pins
key order, number formatting, and string escaping (RFC 8785). Lowercase
hex is required on the wire: the JCS signing input includes `cert_hash`
verbatim, so accepting mixed case would force every signer and verifier
to normalize before computing the signing bytes — an easy spec rule to
miss across language ports.
The domain tag lives in the byte stream, not in the canonical JSON, so
an attestation cannot be confused with a same-shape JCS document signed
for a different protocol that uses the same Ed25519 key.
Signers are `did:key` over Ed25519 with the multibase base58btc (`z`)
prefix. Non-base58btc encodings of the same public key are rejected on
verify so allowlists and signer-pinning policies stay consistent across
peers.
## Verification
`Registry` maps type to verifier. `Policy::AcceptKnown` (default) lets
unknown types pass without trust so adoption stays incremental.
`Policy::RequireAll` enforces a per-repo allowlist but stays lenient on
unknown types in the batch — anyone can attach an attestation, so
short-circuiting on an unrelated `attacker/spam/v1` would be a DoS
vector. `Policy::RejectUnknown` is strict.
`AttestedRefUpdateCert::verify_attestations(&Registry)` is the one-call
convenience over `registry.verify_all(&env.attestations, env.cert_hash()?)`.
## Canonical test vectors
Ed25519, JCS, and SHA-256 are all deterministic, so a fixed seed plus a
fixed cert body produce exactly the same `cert_hash`, `signer` DID, and
`sig` across implementations. `tests/canonical_vectors.rs` pins these
values so a port to Go, JS, Python, or any other language has a concrete
oracle to check against without reading Rust. If a vector drifts, that
is a wire-protocol break.
## Tests
42 pass: 33 unit + 4 cross-crate integration against the real
`gitlawb_core::cert::RefUpdateCert` + 4 canonical vector pins + 1
rustdoc quick-start doctest.
`cargo fmt --all -- --check` clean.
`cargo clippy --workspace --all-targets -- -D warnings` clean.
`cargo test --workspace` green; the new crate breaks no existing suite.
## Not in this PR
- Node integration: storage, GraphQL, register flow.
- UI surfacing.
- Trust-score weighting policy for typed attestations.
- Rollout ordering: old nodes silently drop the new `attestations`
field (`RefUpdateBody` does not set `deny_unknown_fields`); signers
and verifiers roll out independently.
- wasm / no_std: not targeted; ed25519-dalek and serde_json are
std-only.
## Reference implementation
End-to-end demo at open-covenant/covenant@64ec780,
`agent-os/examples/gitlawb-attest-demo/` (`covenant/exec/v1`).
Closes #7.
|
||
|
|
a9f208a378 |
Feat/ucan chain validation (#19)
* feat(ucan): validate proof chains and attenuation * feat(auth): validate UCAN chain in middleware * feat(server): enforce UCAN chain on write routes * docs(ucan): clarify wildcard attenuation semantics in doc comment * test(node): add test-only constructors for Db and RepoStore * fix(auth): harden require_ucan_chain middleware, lower log level, and add tests * refactor(server): centralize and deduplicate auth middleware layering |
||
|
|
491978a809 |
Test/core crypto primitives (#16)
* test(core/cert): add RefUpdateCert validation coverage * test(core/cid): add CID parsing and hash tests * test(core/did): add DID validation and predicate tests * test(core/identity): add keypair and signed payload tests * style(did): wrap long assert! messages to satisfy rustfmt * style(identity): wrap long assert! messages to satisfy rustfmt |
||
|
|
0b056f3ad0 |
[codex] fix Dependabot Rust alerts (#15)
* fix(deps): resolve dependabot rust alerts * docs: clarify dependabot validation |
||
|
|
d366fc6b97 |
[codex] chore: add live-safe OSS readiness pass (#14)
* chore: add live-safe OSS readiness pass * fix(node): verify optional peer signatures during rollout * docs: rewrite README for node roadmap |
||
|
|
b12c6bc328 |
feat: per-DID rate limiting on creation endpoints (10/hour) (#13)
Add sliding-window rate limiter applied to resource creation endpoints: - POST /api/v1/repos (repo creation) - POST /api/register (agent registration) - POST /api/v1/repos/:owner/:repo/fork - POST /api/v1/repos/:owner/:repo/issues - POST /api/v1/repos/:owner/:repo/pulls Returns 429 Too Many Requests with Retry-After header when exceeded. Other write operations (merge, close, comment, star) are not limited since they're bounded by existing resources. Includes 5 unit tests covering: within-limit, over-limit, per-key isolation, window expiry, and cleanup of stale entries. Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> |
||
|
|
14a1cdd656 |
chore: bump v0.3.9, fix all clippy warnings, enforce strict CI lint (#12)
* chore: bump v0.3.9, fix all clippy warnings, enforce strict CI lint - Bump workspace version 0.3.8 → 0.3.9 - Fix 22 clippy warnings across 17 files: - Replace .last() with .next_back() on DoubleEndedIterators (8 instances) - Remove useless .into() conversions on anyhow::Error (3) - Remove needless borrows in gl init (3) - Replace map_or with is_some_and/is_none_or (3) - Collapse consecutive str::replace into char array pattern (2) - Change &PathBuf to &Path in function signatures (2) - Remove dead code: unused init_bare_repo function - Collapse if-let into outer match arm - Replace vec![...] with array literal - Introduce RecordAnchorInput struct (10 params → 1 struct) - Fix always-true boolean condition in gossip task guard - Promote clippy from advisory to strict (-D warnings) in CI Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * style: cargo fmt Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>v0.3.9 |
||
|
|
be099386e5 | Update bootstrap-peers.json (#5) | ||
|
|
b09714af0b |
fix(doctor): detect git-remote-gitlawb.exe on Windows (#10)
which_in_path() now also checks for `{name}.exe` on Windows targets,
so `gl doctor` correctly finds git-remote-gitlawb on that platform.
Co-authored-by: kelvinzer0 <[email protected]>
Co-authored-by: OpenClaude (mimo-v2.5-pro) <[email protected]>
|
||
|
|
d09c23da86 |
feat: macOS menu bar app for managing gitlawb-node Docker Compose stack (#8)
- Menu bar icon with colored status dot (green/yellow/red) - Start/Stop node via Docker Compose - Settings UI: ports, public URL, postgres password, auto-sync, operator config - View Logs window with live docker compose logs - Auto-start on launch and login (SMAppService) - Docker detection with install prompt - Repo root detection via compile-time path for local builds - Port variables wired through docker-compose.yml for custom port support - GITLAWB_AUTO_SYNC passthrough for peer replication toggle - Build script: scripts/build-macos-app.sh (codesign + DMG) Co-authored-by: Rimas Mocevicius <[email protected]> |
||
|
|
f3df4a4dd4 |
Resolve bare DID aliases for trust and cert lookups (#9)
* Resolve bare DID aliases for trust and cert lookups Public profile and CLI surfaces accept bare z6Mk keys, but trust and certificate data are stored against full did:key identities. Normalize agent profile lookups and make cert commands resolve owner-less repos against the caller DID so valid pushes are not hidden behind node-owned paths. Constraint: Public URLs and CLI examples commonly use short z6Mk identifiers Constraint: Ref certificates are issued after push and stored under the owner repo id Rejected: Register duplicate bare-key identities | would split trust and certificate history across aliases Rejected: Require full certificate UUIDs in show output | list already displays short IDs as the human-facing handle Confidence: high Scope-risk: narrow Directive: Keep bare key inputs as aliases for did:key, not separate identities Tested: cargo check -p gl Tested: cargo run -q -p gl -- cert list agentbot-opensource Tested: cargo run -q -p gl -- cert show agentbot-opensource 3686b1fe Tested: cargo check -p gitlawb-node Tested: cargo test -p gitlawb-node normalize_agent_did * Preserve agent rank on short profile URLs The public profile route can use compact handles like z6MkpUq1, while trust history is stored under the full did:key identity. Resolve exact aliases first, then fall back to matching registered agent key prefixes so older high-level identities are not displayed as fresh newcomers. Constraint: Public GitLawb profile URLs expose short z6Mk handles. Constraint: Trust and push history remain keyed by canonical did:key identities. Rejected: Mint duplicate short-handle agent rows | would split rank and push history across aliases. Confidence: high Scope-risk: narrow Directive: Keep short profile handles as aliases for canonical did:key identities, never as separate trust principals. Tested: cargo fmt --check -p gitlawb-node Tested: cargo test -p gitlawb-node agent Tested: cargo check -p gitlawb-node Tested: cargo check -p gl |
||
|
|
034cb3c80c |
feat: replica registration protocol (Phase 2 v1) (#4)
Phase 2 of the network-resilience plan: any node can register itself as a
replica of someone else's repo, and the origin exposes the replica list
publicly as a "your repo is mirrored by N nodes" trust signal.
What ships
- New repo_replicas table keyed by (repo_id, replica_did) with replica URL
and registration timestamp. UNIQUE on (repo_id, replica_did); duplicate
registrations update the URL.
- API on origin:
PUT /api/v1/repos/:owner/:repo/replicas (auth) register
DELETE /api/v1/repos/:owner/:repo/replicas (auth) unregister
GET /api/v1/repos/:owner/:repo/replicas (public) list
Origin rejects self-registration and validates the replica URL (http(s),
no whitespace/control chars, length-bounded).
- CLI:
gl repo replica-register <owner>/<repo> --url <my-public-url>
gl repo replica-unregister <owner>/<repo>
gl repo replicas <owner>/<repo>
- gl repo info now shows "Replicas: N" when the origin supports it.
- Tests: 5 new unit tests for the URL validator. Full node suite at 51 pass,
full gl suite at 188 pass.
Deferred to Phase 2.5
- Replica auto-pulls the repo on registration (today: operator runs
git clone gitlawb://... manually after registering).
- Periodic freshness sync worker driven by the replica list.
- Gossipsub subscription to push ref-update events to replicas live.
Co-authored-by: OpenClaude <[email protected]>
|
||
|
|
d2a440e521 |
Feat/backport perf fixes (#11)
* feat: replica registration protocol (Phase 2 v1)
Phase 2 of the network-resilience plan: any node can register itself as a
replica of someone else's repo, and the origin exposes the replica list
publicly as a "your repo is mirrored by N nodes" trust signal.
What ships
- New repo_replicas table keyed by (repo_id, replica_did) with replica URL
and registration timestamp. UNIQUE on (repo_id, replica_did); duplicate
registrations update the URL.
- API on origin:
PUT /api/v1/repos/:owner/:repo/replicas (auth) register
DELETE /api/v1/repos/:owner/:repo/replicas (auth) unregister
GET /api/v1/repos/:owner/:repo/replicas (public) list
Origin rejects self-registration and validates the replica URL (http(s),
no whitespace/control chars, length-bounded).
- CLI:
gl repo replica-register <owner>/<repo> --url <my-public-url>
gl repo replica-unregister <owner>/<repo>
gl repo replicas <owner>/<repo>
- gl repo info now shows "Replicas: N" when the origin supports it.
- Tests: 5 new unit tests for the URL validator. Full node suite at 51 pass,
full gl suite at 188 pass.
Deferred to Phase 2.5
- Replica auto-pulls the repo on registration (today: operator runs
git clone gitlawb://... manually after registering).
- Periodic freshness sync worker driven by the replica list.
- Gossipsub subscription to push ref-update events to replicas live.
Co-Authored-By: OpenClaude <[email protected]>
* perf: backport query optimizations + add Fly test deployment
- Add expression indexes (owner_short+name, updated_at DESC) for fast repo listing
- Add list_all_repos_with_stars() batch query eliminating N+1 star count calls
- Add list_all_repos_paged() with DISTINCT ON dedup + pagination + X-Total-Count header
- Add prune_self_peers() to remove stale self-referencing peer rows at startup
- Add self-loop prevention in HTTP peer notifications during git-receive-pack
- Add SwarmStatus observability to p2p_info endpoint (connected/mesh/all peers)
- Add fly.toml for gitlawb-node-test deployment in iad region
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: OpenClaude <[email protected]>
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
|
||
|
|
f312955c35 |
feat: replica registration protocol (Phase 2 v1) (#3)
Phase 2 of the network-resilience plan: any node can register itself as a
replica of someone else's repo, and the origin exposes the replica list
publicly as a "your repo is mirrored by N nodes" trust signal.
What ships
- New repo_replicas table keyed by (repo_id, replica_did) with replica URL
and registration timestamp. UNIQUE on (repo_id, replica_did); duplicate
registrations update the URL.
- API on origin:
PUT /api/v1/repos/:owner/:repo/replicas (auth) register
DELETE /api/v1/repos/:owner/:repo/replicas (auth) unregister
GET /api/v1/repos/:owner/:repo/replicas (public) list
Origin rejects self-registration and validates the replica URL (http(s),
no whitespace/control chars, length-bounded).
- CLI:
gl repo replica-register <owner>/<repo> --url <my-public-url>
gl repo replica-unregister <owner>/<repo>
gl repo replicas <owner>/<repo>
- gl repo info now shows "Replicas: N" when the origin supports it.
- Tests: 5 new unit tests for the URL validator. Full node suite at 51 pass,
full gl suite at 188 pass.
Deferred to Phase 2.5
- Replica auto-pulls the repo on registration (today: operator runs
git clone gitlawb://... manually after registering).
- Periodic freshness sync worker driven by the replica list.
- Gossipsub subscription to push ref-update events to replicas live.
Co-authored-by: OpenClaude <[email protected]>
|
||
|
|
ec4ef81770 |
feat: embed bootstrap peers seed list for automatic network discovery (#1)
* feat: embed bootstrap peers seed list for automatic network discovery A fresh \`docker compose up\` now joins the Gitlawb network with zero manual peer configuration. The node parses an embedded \`bootstrap-peers.json\` on startup and merges the entries into both the HTTP gossip task and the libp2p Kademlia bootstrap. - Add \`bootstrap-peers.json\` at repo root (versioned schema, PR-friendly) - New \`bootstrap\` module in the node crate (parse + merge_seeds) - Wire into \`main\` after \`Config::parse\` - Operators can opt out via \`GITLAWB_BOOTSTRAP_DISABLE_SEEDS\` for isolated dev networks Also in this commit: - \`cargo fmt --all\` over the entire workspace (no logic changes) - Downgrade CI clippy step to advisory (\`continue-on-error: true\`) until the existing lint backlog is cleared. fmt + tests stay strict. Co-Authored-By: OpenClaude <[email protected]> * test: cover bootstrap merge logic with unit tests Refactor bootstrap.rs into pure functions (parse_seed_list, merge_into_vecs) so the parse + merge logic can be tested without constructing a Config or mutating process-global env vars. Adds 11 tests covering: - valid v1 list parses - unknown version is rejected - malformed JSON is rejected - empty / missing peers array - merge appends new http + p2p entries - merge dedupes against existing entries - invalid p2p_multiaddr is skipped (http still added) - empty strings are skipped - null optional fields are tolerated - the canonical bootstrap-peers.json shipped in the repo always parses (regression guard against future schema changes) Co-Authored-By: OpenClaude <[email protected]> * fix(security): reject path traversal in repo_store local_path Closes CodeQL alert #4 (Uncontrolled data used in path expression). Both `owner_did` and `repo_name` come from URL parameters and were used unsanitized to build a filesystem path, so a request like `/did:key:foo/../../../etc/passwd.git/info/refs` could escape the repos directory. Fix: - New `validate_owner_did` and `validate_repo_name` enforce a strict allowlist before path construction (alphanumeric + `: . _ -` for DIDs; alphanumeric + `. _ -` for repo names; rejects empty, `..`, leading `.` or `-`, slashes, backslashes, null bytes, overlong inputs). - `RepoStore::local_path` now returns `Result<…>` and the 5 callers propagate the error. - Defence in depth: even after the allowlist passes, the joined path is checked to still be rooted at `repos_dir`. - 17 unit tests cover normal DIDs/repo names plus every malicious shape (`..`, `/`, `\`, leading dot/dash, null byte, overlong, did:web with dots). Co-Authored-By: OpenClaude <[email protected]> * fix(security): add Path::components barrier for CodeQL rust/path-injection CodeQL's rust/path-injection query did not recognise the existing allowlist validator as a sanitiser, so it kept flagging local_path even though every malicious input was already rejected. Adds an explicit Path::components() walk after path construction that rejects any ParentDir or CurDir segment. This is the barrier pattern CodeQL recognises — and is also genuine defence-in-depth in case a future change ever weakens the allowlist. Allowlist + starts_with + components walk = three independent layers between attacker-controlled HTTP input and filesystem access. All 17 existing tests still pass. Co-Authored-By: OpenClaude <[email protected]> --------- Co-authored-by: OpenClaude <[email protected]> |
||
|
|
068fc76899 |
fix: nuke dummy binaries + fingerprints before second cargo build
Touch alone wasn't enough — cargo can still reuse cached fingerprint state from the dependency-cache layer. Explicitly remove the dummy binaries and their fingerprint dirs so the second build reproduces real artifacts unconditionally. Also drop the obsolete `version: '3.9'` from docker-compose.yml. Co-Authored-By: OpenClaude <[email protected]> |
||
|
|
49dadf5172 |
fix: force cargo rebuild in docker layer
Touch real source files after COPY so cargo's fingerprint check
detects the change and rebuilds from real source. Without this,
cargo can keep the dummy `fn main() {}` binaries from the
dependency-cache layer, causing the runtime container to exit
immediately with code 0.
Co-Authored-By: OpenClaude <[email protected]>
|
||
|
|
6b9c5278d4 |
chore: initial public release
Open-source release of the gitlawb node, CLI, and git remote helper. - gitlawb-node: Axum HTTP server with git smart-HTTP and DID auth - gl: command-line client (identity, repos, PRs, MCP server) - git-remote-gitlawb: git remote helper for gitlawb:// URLs - gitlawb-core: shared crypto primitives (DID, CID, HTTP sigs, UCAN) Dual licensed under MIT or Apache-2.0. Co-Authored-By: OpenClaude <[email protected]> |