Skip to content

feat: forge-v2 data plane — repos, pushes, membership on protocol 14 - #18

Merged
PastaPastaPasta merged 6 commits into
masterfrom
feat/forge-v2-data-plane
Sep 25, 2026
Merged

PastaPastaPasta merged 6 commits into
masterfrom
feat/forge-v2-data-plane

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Owner

PR "C" of the forge-v2 client migration. It moves the CLI's git data plane onto forge-v2 (protocol 14). git-remote-dash, dg repo, dg collab and dg repack now create, push to and clone repositories that live as documents in the shared forge-core contract. forge-v1 repositories (one contract each) can still be read but are now read only. Builds on #15 (SDK 4.2); rebased on #14 / #16.

Model

  • RepoRef (forge_core::scope) is either V2 { forge, repo_id, owner_id, name, visibility } or V1 { contract_id, owner_id, name } (read only). Every write starts with require_v2(), which fails with E605 before anything is signed on a v1 repo.
  • DocScope is the contract plus an optional repoId.
    • On v2, every query starts with repoId == R, which is the first property of every forge-core index, and every write carries repoId.
    • Chunk reads are scoped to one uploader's copy ($ownerId). The locator format is platform://<core>/<repoId>/<owner>/<packHash>.
    • A small field codec reads both generations: uris, protectedPatterns and backend.uris are typed string arrays on v2 and JSON strings on v1.
  • Resolution (forge_core::resolve):
    • dash://owner/name: check the name as a slug with rules::v2, then look up the forge-core repo by ($ownerId, name).
    • It falls back to the v1 registry only when the v2 read proves the repo is absent. A failed read is an error, never a fallback.
    • dash://<id>: try a v2 repo document id first, then a v1 repo contract id. The forge contracts themselves and unrelated contracts are refused.
    • On a network with no v2 deployment, v2 operations fail with E702 ("forge-v2 isn't deployed on testnet yet …") and v1 reads keep working.
  • Repo create (forge_core::create) is one journaled, resumable session: it writes repo, then the owner's own maintainer document, then the first config.
    • Each signed transition is saved to $XDG_STATE_HOME/dash-forge/journals before it is broadcast.
    • On resume, a saved transition is replayed from the same bytes and then confirmed. One that provably never landed is discarded. Otherwise an existing document is adopted (a private repo is refused), and only if there is neither is a new transition signed.
    • Re-running a finished create writes nothing and costs 0. Only public repositories can be created.
    • The repo-v1 contract path is removed: REPO_V1_TEMPLATE, the solo-owner token rules, contract_create, token writes, and orphan-contract recovery.
  • Membership (forge_core::members):
    • Grant means the owner creates a writer/maintainer document, revoke means the owner deletes it, and list queries by repoId.
    • dg collab add|remove|list work on v2. suspend/unsuspend explain that remove revokes access immediately.
    • tokens.rs is now a read-only v1 collaborator list.
  • Push authorization: consensus is the gate.
    • ownerRefersTo refuses a non-member's refUpdate, chunk or packManifest with 40120. That becomes Error::NotAMember { document_type, .. } and E601, or "only maintainers …" on a maintainer-only type.
    • The helper's advisory pre-check is a membership read that points at dg collab add. It also refuses a writer updating a protected ref before any pack is paid for. DASH_FORGE_SKIP_WRITE_PRECHECK=1 still bypasses it.
  • Shared nonce: on forge-v2 every write an identity makes shares one nonce counter, so a consumed nonce is no longer treated as success. Creates and deletes confirm with a proved read and re-prepare (BroadcastOutcome::NonceConsumed). The push chunk journal is keyed by repo and uploader.
  • Pack copies:
    • fetch, repack and reseed read each pack from its best verifying copy, in FORGE_RULES_V2 order (maintainers, writers, others).
    • A copy nobody can read no longer blocks an honest push: the pusher stores its own copy, which is why $ownerId is in the pack indexes.
    • A non-member's chunkless storage = 0 manifest cannot fail a clone.
  • Pack list: a new shared pure rule, v2_pack_list (Rust + TypeScript, 13 conformance vectors, forge-v2.md §4).
    • A pack sits at its first upload and is represented by its top-ranked copy (kind and metadata).
    • packRef counts within its kind, and supersedes only binds from a verified pack.
    • Superseded packs keep their positions, unlike v1, which drops them, so no locator is ever renumbered and an unverified claim cannot hide packs.
    • locator_pack_space is built on it. It is its own commit (7073326) so the web port (PR E, feat(web): read forge-v2 repos (protocol 14) with v1 read-compat #17) can share it.
  • Repack only consolidates: it writes a superseding pack and manifest and deletes nothing, because chunks and manifests are permanent on v2. dg repack no longer promises a refund, and dg repo delete is gone.
  • BYO storage (feat: git push stores packs on your own storage (S3 SigV4, IPFS, Platform) with N-of-M replication #12) works unchanged on v2. Manifest uris is a typed array, budgeted at 8 × 300 bytes (UriBudget).
  • Private-repo seams (forge_core::private): RefNameHasher, RepoCodec, PackCipher and RepoKeyReader, with only the public implementation. Private repos are refused until the private-repo PR.
  • Master's keyset ref scan (refs.rs) is now scoped by DocScope.
  • Issues, PRs, releases and labels still address v1 repo contracts via RepoRef::v1_contract_id(); forge-collab is PR D.

Live verification (devnet moutai, protocol 14)

Check Result
dg repo create (repo + maintainer + config) 0.00096–0.00131 DASH per repo; re-run → exists, 0 credits
git push / git clone round-trip byte-identical, fsck --strict clean
writer on a protected ref refused at consensus (protectedRefUpdate is maintainer-only) with the typed "only maintainers" error; the same writer's unprotected ref lands
grant writer → writer pushes → revoke → push writer push lands; after revoke rejected at consensus with 40120 (… document type writer … not found for path $ownerId)
non-member push rejected at consensus with 40120
repack consolidated pack + manifest + locator written; manifests 4 → 6, nothing deleted; clone byte-identical afterwards; 0.00277 DASH
one small push (Platform chunks + manifest + locator + refUpdate) 0.00319 DASH
grant / revoke a writer 0.00039 DASH / 0.00021 DASH refunded
BYO storage (e2e/cli/storage-byo.sh, local MinIO + kubo) PASS (see below)
testnet dash:// v1 clone clones by name and by contract id; a push is refused as read only (E605)
e2e/cli/run.sh on moutai 8/8 PASS: 01 round-trip, 02 non-ff, 03 ref delete, 04 revoked-writer push (40120), 05 non-member push (40120), 06 third-party verify, 07 depth/filter, 08 testnet v1 read-compat

BYO storage checks, all passing:

  • the external-only manifest is written with typed uris;
  • a reader with no S3 profile clones from the public copies;
  • an N-of-M failure leaves refs untouched;
  • reseed --from-local restores the recorded copy.

Review

An independent review covered the authorization assumptions, resumability and double-paying, ambiguity in URL resolution, and v1 read-compat. It found these problems:

  • a consumed nonce was treated as success on the shared contract;
  • the push journal was not keyed by repo and uploader;
  • a dead or hostile copy could block an honest push;
  • unverified supersedes claims could hide packs;
  • a writer pushing a protected ref paid first and was then told they were "not a writer";
  • a fatal replay could strand the create journal;
  • create could adopt a private repo.

All are fixed, with unit tests, in 52a7e87, 5654715 and da57f7c. A re-review of the fixes confirmed them; its nits (refuse only the protected refs of a writer's push, a longer landing check) are in da57f7c. A simplification pass is folded into the feature commit: create_landed now goes through create_journaled, plus role_doc, readable(), and removal of the dead v1 helpers.

Gates

  • Rust: cargo fmt --all -- --check, cargo clippy --locked --workspace --all-targets -- -D warnings and cargo test --locked --workspace all pass.
  • forge-web: pnpm typecheck, lint on the touched files, the conformance suite (206/206) and pnpm build all pass. The only web changes are the v2PackList port and its conformance case.

e2e / CI changes

  • The CLI suite runs on devnet moutai, against an OWNER-owned e2e-cli repo created on first use.
  • Scenarios 04 and 05 are rewritten for membership (40120), and scenario 08 (the testnet v1 clone) is new.
  • The nightly needs new secrets: FORGE_MOUTAI_IDENTITY_OWNER, FORGE_MOUTAI_IDENTITY_COLLAB and FORGE_MOUTAI_IDENTITY_CONTRIB, plus the existing FORGE_TEST_IDENTITY_CONTRIB for scenario 08. Until they exist, the suite job reports skipped.
  • The browser read fixture (m1-5124) is a testnet v1 repo. Because v1 is read only, seed-read-fixture.sh now only verifies it; the fixture moves to forge-v2 with the web app.

Known gaps

  • forge-collab on v2 (issues, PRs, releases, labels, fork) is PR D. On a v2 repo those commands say so.
  • Private repositories are refused, though the seams are in place.
  • dg migrate (v1 → v2) is not written yet.
  • Testnet has no forge-v2 deployment, so dg repo create there fails with E702.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 26 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 0d6c15f5-d280-47d3-9790-9208d74e91d5

📥 Commits

Reviewing files that changed from the base of the PR and between cb6d648 and 5578d5f.

📒 Files selected for processing (59)
  • .github/workflows/testnet-nightly.yml
  • Makefile
  • crates/dg/src/collab.rs
  • crates/dg/src/common.rs
  • crates/dg/src/cost.rs
  • crates/dg/src/errors.rs
  • crates/dg/src/fmt.rs
  • crates/dg/src/issue.rs
  • crates/dg/src/main.rs
  • crates/dg/src/maint.rs
  • crates/dg/src/pr.rs
  • crates/dg/src/release.rs
  • crates/dg/src/repo.rs
  • crates/dg/src/storage.rs
  • crates/forge-core/src/backends.rs
  • crates/forge-core/src/backends/live_tests.rs
  • crates/forge-core/src/backends/platform.rs
  • crates/forge-core/src/collab.rs
  • crates/forge-core/src/create.rs
  • crates/forge-core/src/error.rs
  • crates/forge-core/src/lib.rs
  • crates/forge-core/src/members.rs
  • crates/forge-core/src/platform.rs
  • crates/forge-core/src/private.rs
  • crates/forge-core/src/refs.rs
  • crates/forge-core/src/repo.rs
  • crates/forge-core/src/resolve.rs
  • crates/forge-core/src/scope.rs
  • crates/forge-core/src/storage/mod.rs
  • crates/forge-core/src/storage/targets.rs
  • crates/forge-core/src/tokens.rs
  • crates/forge-core/src/user_error.rs
  • crates/forge-core/tests/collab_tokens.rs
  • crates/forge-core/tests/repo_lifecycle.rs
  • crates/forge-import/src/estimate.rs
  • crates/forge-import/src/importer.rs
  • crates/git-remote-dash/src/admin.rs
  • crates/git-remote-dash/src/helper.rs
  • crates/git-remote-dash/src/journal.rs
  • crates/git-remote-dash/src/main.rs
  • crates/git-remote-dash/src/url.rs
  • docs/contracts/forge-v2.md
  • docs/errors.md
  • e2e/README.md
  • e2e/cli/config.sh
  • e2e/cli/lib.sh
  • e2e/cli/run.sh
  • e2e/cli/scenarios/01-round-trip.sh
  • e2e/cli/scenarios/02-non-ff.sh
  • e2e/cli/scenarios/03-ref-delete.sh
  • e2e/cli/scenarios/04-frozen-push.sh
  • e2e/cli/scenarios/04-revoked-writer-push.sh
  • e2e/cli/scenarios/05-no-token-push.sh
  • e2e/cli/scenarios/05-non-member-push.sh
  • e2e/cli/scenarios/06-third-party-verify.sh
  • e2e/cli/scenarios/07-depth-and-filter.sh
  • e2e/cli/scenarios/08-v1-read-compat.sh
  • e2e/cli/seed-read-fixture.sh
  • e2e/cli/storage-byo.sh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

PastaPastaPasta and others added 6 commits September 25, 2026 13:26
Repositories are now forge-v2: a repo document plus the owner's maintainer membership and an initial config in the shared forge-core contract, created by one journaled, resumable session. Refs, config, pack manifests and chunks are addressed through a DocScope that adds repoId to every query and write; v1 repositories resolve read-only through the registry.

Membership is writer/maintainer documents (dg collab add/remove/list); consensus refuses a non-member's write with 40120. Fetch reads each pack from its best verifying copy in the FORGE_RULES_V2 order, and repack consolidates without deleting anything.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
locator_pack_space and the push-index plan now use the shared rule: one entry per pack hash at its first upload, ranked representative copy, packRef counted within kind 0. Superseded packs keep their positions (v2 manifests are permanent), so a repack appends instead of renumbering; repack, reseed and fetch read every git pack from its best copy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r secret

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…eded

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…copies

Review follow-ups: a consumed nonce is no longer taken as success (forge-v2 writes share one nonce counter per identity) — creates and deletes confirm by a proved read and re-prepare; the push chunk journal is keyed by repo and uploader; a dead or hostile copy of a pack no longer blocks an honest push (the pusher stores its own); a writer pushing a protected ref is refused before paying, and 40120 on a maintainer-only type says so; a create replay that provably never landed is discarded instead of stranding the journal, and a private repo is never adopted; fetch falls back to time order without the member list and ignores non-members' chunkless manifests; reseed skips unreadable packs; v1 names resolve to the slug.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r landing check

Re-review follow-ups: a writer's push refuses just the refs matching protected patterns (deletes included) and pushes the rest; the proved-read check after a consumed nonce waits ~15 s before signing a replacement; the maintainer-only error suggests an unprotected branch only for refs. The live lifecycle test now covers a writer on a protected ref (typed protectedRefUpdate refusal) and an unprotected one (lands).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@PastaPastaPasta
PastaPastaPasta merged commit d36edf6 into master Sep 25, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant