Skip to content

Consolidate libforge and the satellite services into the repository - #7

Draft
Peeja wants to merge 8 commits into
claude/forge-consolidationfrom
claude/forge-consolidation-2
Draft

Consolidate libforge and the satellite services into the repository#7
Peeja wants to merge 8 commits into
claude/forge-consolidationfrom
claude/forge-consolidation-2

Conversation

@Peeja

@Peeja Peeja commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

[From Claude:]

Stacked on #6 (claude/forge-consolidation); review and merge that first. This PR is the consolidation itself: the code that libforge held becomes three in-repo modules, three satellite services join, the five existing services are rewired onto the in-repo modules, and CI, the container build and the root Makefile stop assuming a fixed set of four services. Nothing outside this repository changes: libforge, guppy, delegator, piri-signing-service, indexing-service and go-ipni-tools keep their repositories and pipelines until the team decides to retire them.

Eight commits, each meant to be reviewed on its own — the commit messages carry the detail; this description is the map.

Commits

# Commit What it adds
1 Add protocol: the wire contract as an in-repo module protocol/ (module github.com/fil-forge/forge/protocol): commands/**, blobindex, receipt, retrieval, bytemap, digestutil from libforge main 81372e7 plus PR fil-forge/libforge#52 (f4b13f7, rebased). Its own Makefile with the codegen gate (make codegen-build gen-check).
2 Add internal: repo-private helpers shared by the services internal/ (module github.com/fil-forge/forge/internal): jobqueue, identity, piece (libforge); sigv4, s3perm, client/hilt (from live hilt, which is newer than libforge#52's copies); zapucan (libforge#52); client/delegator (delegator's client/); pdpsigner/{types,signer,inprocess,client} (piri-signing-service's pkg/*); ipni/advertisement (a copy of go-ipni-tools' pkg/advertisement, see below). The Go toolchain enforces the internal/ rule across modules under github.com/fil-forge/forge/, so this module is repo-private by construction.
3 Add attestation: the did:mailto attestation extension as its own module attestation/: attestation, didmailto, attest (was libforge/commands/ucan/attest). Depends on ucantone only.
4 Add delegator, piri-signing-service and indexing-service as services From their current mains (3d3525c, 8472ad9, a119ea8), module paths rewritten, per-repo .github/, Dockerfile*, deploy/, .storoku.json dropped (source only; each storoku pipeline stays in its repo). indexing-service's tracked 24 MB Lambda bootstrap binary is dropped too. The library halves of delegator and piri-signing-service live in internal/ (commit 2).
5 Rewire the services onto the in-repo modules piri, hilt, sprue, ingot, smelt (and the stress tester) import protocol/internal/attestation instead of libforge; hilt drops the copies that moved (pkg/sigv4, pkg/s3perm, pkg/lib/zapucan, the S3 RPC client, the auth/bucket error sentinels) and keeps its server side, AdminClient and upload client. go.mods lose the external libforge/hilt/smelt/indexing-service requires and gain replace directives to the in-repo modules. Prose that still said "libforge" is updated.
6 smelt: follow the shared modules and the root build context pkg/workspace rebuilds every service when any of protocol, internal, attestation is in the workspace (was: libforge); pkg/stack builds images with the repository root as context; tests/compat's otherThan maps service names explicitly and panics on an unknown one instead of vacuously passing.
7 Build and test every module in CI; images from the repository root ci.yml: a replaces job (.github/scripts/check-replaces.sh), a static 12-module unit matrix with the codegen gate on protocol/attestation, and the stack job building all 7 service images with context: .. docker/Dockerfile becomes a root-context build (SERVICE, MAIN_PKG, BIN, BUILD_TAGS). publish-ghcr.yml publishes 7 services. Root Makefile (SERVICES, LIBRARIES, gen-check, check-replaces), .dockerignore, go.work (12 modules), README.md.
8 smelt: probe the satellites' health with a GET, not wget --spider What the first stack run on this branch found: the shared debian image ships GNU wget, whose --spider probes with HEAD (busybox's, in the satellites' alpine images, probes with GET), and echo answers HEAD on a GET-only health route with 405 — so the signer (and the delegator, identically built) came up "unhealthy" and compose up aborted. The three satellite probes become wget -q -O /dev/null …, a GET under both wgets with the same pass/fail semantics; reproduced against the signer's echo version before pushing.

How the pieces relate

protocol ──► ucantone            (wire contract; the only module downstream code needs)
internal ──► protocol            (repo-private helpers)
attestation ──► ucantone
piri, hilt, sprue, ingot, delegator, piri-signing-service, indexing-service ──► protocol, internal, attestation (as needed)
hilt ──► smelt/pkg/stack         (its integration tests)
ingot ──► indexing-service       (the indexer's query client)
smelt ──► protocol, internal
  • Development uses go.work; CI and the container build use GOWORK=off with replace directives, so each module is hermetic on its own go.mod. A missing replace resolves github.com/fil-forge/forge/... through the proxy to the repository root and fails confusingly ("module found but does not contain package"), which is what check-replaces.sh guards against.
  • The S3 wire contract (protocol/commands/s3{,/request,/bucket}: Operation, OperationFor, ClassifyRequest, the error sentinels) is one module, so hilt, ingot and internal/client/hilt share identical types — the design of libforge#52, realised in-repo with live hilt's newer sigv4/s3perm/client code. ErrBucketAlreadyOwned (live hilt) is ported into protocol/commands/s3/bucket.
  • ucantone is pinned to v0.0.0-20260904190501-7fca40e13941, the head of Add ucanlib (from libforge) and fixed test identities to testutil ucantone#57 (the fixtures and ucanlib that the rest of the tree needs). The pseudo-version resolves through the module proxy today; bump it once #57 merges.

Deliberately not in this PR

  • release.yml is untouched. Per-service tags for the three new services (and whether the library modules get tags at all) need a decision first.
  • Deploy pipelines of delegator, piri-signing-service and indexing-service stay in their repositories and keep deploying from there; pointing them at images built here is a follow-up.
  • The client (guppy's pkg/client/locator/tokenstore and ingot's forgeclient fork) — Phase 4, in Add forgeclient: ingot's Forge client as a shared module #8.
  • go-ipni-tools still depends on libforge: pkg/queue imports libforge/jobqueue and its tests use libforge/testutil. Forge therefore carries its own copy of pkg/advertisement under internal/ipni/advertisement. Once go-ipni-tools moves pkg/queue onto its own job queue (or internal/jobqueue's successor) and its tests onto ucantone/testutil, the copy can go back to an import.
  • swarf stays an external module and still pins libforge, so libforge remains an indirect dependency of hilt and ingot (via swarf/pkg/client) and of indexing-service (via go-ipni-tools/pkg/queue). No in-repo package imports it.
  • No apidiff gate and no go work sync check yet.

Verification (this branch, GOWORK=off unless noted)

  • All 12 modules: go build ./..., go vet ./..., go mod tidy leaves no diff (piri with -tags skiff, as CI builds it).
  • Tagged suites compile: smelt -tags itest ./tests/s3, -tags e2e ./tests/e2e, -tags compat ./tests/compat; hilt -tags itest ./itest.
  • make -C protocol codegen-build gen-check, make -C attestation codegen-build gen-check: generated codecs are current.
  • .github/scripts/check-replaces.sh: every in-repo require has a matching replace.
  • Workspace mode (go.work): every module builds.
  • Unit tests pass for every module except the packages that need a Docker daemon (smelt pkg/stack image builds, indexing-service pkg/aws and pkg/redis testcontainers, piri's minio store), which this sandbox has no daemon for; CI runs them.
  • CI on this branch so far: the replace check and all 12 unit jobs are green, and the stack job built all seven images from the root context. Its e2e step failed on the health probe described under commit 8; the stack job re-runs with that fix.

Reviewing

Commit by commit. Commits 1–4 are mostly file moves: git show --stat -M shows the moves, and each can be checked against its source (diff -r against libforge at 81372e7+PR 52, or the satellite's main at the SHA above; the differences are module paths, dropped deploy material and gofmt). Commit 5 is the one with hand edits, concentrated in hilt; commit 7 is the CI/Docker/Makefile change; commit 8 is four files.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CGAGAib517Ae1kg8SCdcEt

github.com/fil-forge/forge/protocol carries what libforge published as
the network's wire contract, taken from libforge main (81372e7) plus its
feat/hilt-s3-client branch (928cf2a, libforge PR #52): commands/** (every
command binding except ucan/attest, which belongs with attestation),
blobindex, receipt, retrieval (was ucan/retrieval), and the two small
helpers they depend on, bytemap and digestutil. Import paths change from
github.com/fil-forge/libforge/<pkg> to
github.com/fil-forge/forge/protocol/<pkg>; the code does not, with one
exception: commands/s3/bucket/errors.go is hilt main's newer copy of the
bucket error sentinels (it adds ErrBucketAlreadyOwned), not PR #52's.

The module depends only on ucantone and third-party libraries — on none
of the other in-repo modules — so anything can consume it. ucantone is
pinned to the branch that took libforge's ucan package
(fil-forge/ucantone#57); re-pin to main once that merges.

Codecs are generated (cbor-gen + dag-json-gen): `make gen` regenerates,
`make codegen-build gen-check` is the CI gate. Both pass here.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGAGAib517Ae1kg8SCdcEt
github.com/fil-forge/forge/internal collects the packages the services
share that are not part of the wire contract. The path makes the boundary
compiler-enforced: only code under github.com/fil-forge/forge/ can import
them.

From libforge main (81372e7): jobqueue, identity, piece. From hilt main
(f842aba), the newest copies of the S3 machinery that libforge PR #52 had
extracted from an older hilt: sigv4, s3perm and hilt's RPC client, here
client/hilt (PR #52's copies lacked WithBaseProofs, blob.Remove in the
write grant, and the current ecdsa API). From PR #52 itself: zapucan.
From delegator main (3d3525c): client -> client/delegator. From
piri-signing-service main (8472ad9): pkg/types, pkg/signer, pkg/inprocess
and pkg/client -> pdpsigner/{types,signer,inprocess,client} — piri runs
the signer in-process in dev mode, so all four move and the service keeps
only its server. From go-ipni-tools main (5f11c95): pkg/advertisement ->
ipni/advertisement, the Forge->IPNI adapter that depends on
commands/assert; its tests get local copies of the two helpers they had
taken from go-ipni-tools' own internal/testutil.

Import paths change; the code does not, apart from the package names of
the two client packages (client -> hilt, delegator) so call sites read as
hilt.New and delegator.New.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGAGAib517Ae1kg8SCdcEt
github.com/fil-forge/forge/attestation is libforge's attestation and
attestation/didmailto packages plus commands/ucan/attest (here attest/),
from libforge main (81372e7). They form one extension — the attest
command is the proof the attestation signer issues and the didmailto
resolver consumes — and nothing in it is Forge-specific, so the module
requires only ucantone and multiformats (plus the codec generators attest
uses); never protocol or internal.

attest.ProofOK used to alias commands.Unit; it now aliases a local Unit
with its own generated codecs so the module stands alone. The one test
that used libforge/identity builds its did:web document from ucantone's
did and multikey primitives instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGAGAib517Ae1kg8SCdcEt
Three services that were standalone repositories join as modules
github.com/fil-forge/forge/{delegator,piri-signing-service,indexing-service},
from fil-forge/delegator 3d3525c,
fil-forge/piri-signing-service 8472ad9 and
fil-forge/indexing-service a119ea8287397e3e013fceebd022988ff4178f86 (their current mains).

Taken the way the original five were: module path rewritten, per-repo
.github/, Dockerfile and Dockerfile.release dropped (CI and images come
from the shared workflows and docker/Dockerfile). Also dropped: deploy/
and .storoku.json — each service's storoku Terraform pipeline stays in
its original repository, which keeps deploying until it is pointed at
images built here — and indexing-service's tracked bootstrap, a 24 MB
aarch64 Lambda custom-runtime binary that was committed alongside its
Terraform and that nothing references. Source only.

What each keeps and what moved out:
- delegator: everything but client/, which became internal/client/delegator
  (piri imports it at operator setup; nothing in delegator imports it).
- piri-signing-service: main.go, pkg/config, pkg/handlers, pkg/server.
  pkg/types, pkg/signer, pkg/inprocess and pkg/client became
  internal/pdpsigner/{types,signer,inprocess,client}; piri uses all four
  (it runs the signer in-process in dev mode), so the library part of the
  service is shared and the service is its HTTP shell.
- indexing-service: everything. Nothing in the repository imports it as a
  library except ingot's use of its query client (pkg/client, pkg/types),
  which now resolves in-repo.

libforge imports point at protocol/ and internal/; go-ipni-tools'
pkg/advertisement at internal/ipni/advertisement; ucantone at the pin the
rest of the repository uses. All three now declare go 1.27.0 (were
1.27.0, 1.25.3 and 1.25.7) because internal/ requires it.

Each Makefile gains the root Makefile's verb contract — vet, image,
image-dev (indexing-service also build and clean, aliasing its indexer
and clean-indexer targets). The images keep the binary names the compose
files and smelt's workspace table expect, via the Dockerfile's BIN arg:
registrar, signer, indexer.

Formatting: the trees are gofmt'd with this repository's Go 1.27 gofmt,
which reflows a handful of upstream files.

Verified standalone (GOWORK=off): build, vet, tidy, tests. indexing-
service's pkg/aws and pkg/redis tests need a Docker daemon (testcontainers)
and are expected to pass in CI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGAGAib517Ae1kg8SCdcEt
Every libforge import in piri, hilt, sprue, ingot and smelt becomes an
import of protocol/ (commands/**, blobindex, receipt, retrieval, bytemap,
digestutil), internal/ (identity, jobqueue, piece, sigv4, s3perm, zapucan,
client/*, pdpsigner/*, ipni/advertisement), attestation/, or ucantone
itself (ucanlib, testutil). libforge disappears from every go.mod; ucantone
is pinned to one version across the repository. The three module
dependencies the live polyrepo had grown between services — ingot on
fil-forge/hilt and fil-forge/indexing-service, hilt and ingot on
fil-forge/smelt for their integration suites — now resolve to the sibling
directories through replace directives instead of to published copies.

hilt is the one service whose code changes beyond import paths: this is
libforge PR #52's extraction, redone against hilt's current code rather
than the 07-31 snapshot that PR took. pkg/sigv4, pkg/s3perm,
pkg/lib/zapucan and the RPC client (pkg/client/client.go) leave for
internal/, and the S3 wire contract — the named error sentinels in
pkg/rpc/service/auth/errors.go and pkg/rpc/service/bucket/errors.go, and
Operation/OperationFor/ClassifyRequest in pkg/rpc/service/auth/operation.go
— leaves for protocol/commands/s3{,/request,/bucket}, where ingot already
consumes it. hilt keeps the server side (the Authorizer and bucket
services, stores, vault), its AdminClient and management/upload clients.
Call sites follow: auth.ErrX -> s3req.ErrX, bucketsvc.ErrX -> s3bkt.ErrX,
Operation -> s3.Operation, classifyRequest -> s3.ClassifyRequest.

Smaller adjustments: sprue's and hilt's internal/testutil aliases follow
ucantone/testutil's name (RandomDigest for what libforge called
RandomMultihash); smelt's tests/s3 imports internal/s3perm, so smelt no
longer needs the PR #52 branch of libforge that the previous commit's
go.mod carried for it; each service's AGENTS.md/CLAUDE.md/README describe
the modules by their new names.

Verified per module (GOWORK=off, piri with -tags skiff): build, vet, tidy
unchanged, tests — the only failures are the tests that need a Docker
daemon (piri's objectstore/minio, smelt's pkg/stack image builds), as on
the base branch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGAGAib517Ae1kg8SCdcEt
Three harness changes for the monorepo shape:

- pkg/workspace: "libforge in the go.work use-list rebuilds every service"
  becomes "protocol, internal or attestation in the use-list rebuilds every
  service" — those are the modules every service compiles in now. The
  error hint for a missing workspace points at the repository's own
  go.work.
- pkg/stack.buildForgeImage builds with the repository root as the Docker
  context. Services reach the shared modules through replace directives,
  and a per-service context cannot see a sibling directory.
- tests/compat.otherThan: the exclusion list it returns is in smelt service
  names (the workspace builder's vocabulary) while the caller's service
  names are repository names; the two differ for sprue, whose smelt
  service is "upload". Comparing them directly excluded nothing for sprue,
  so an "upgrade sprue" case would have run entirely on baseline images
  and passed vacuously. The function now translates.

The developer docs (CLAUDE.md, README.md, docs/DEVELOPING.md,
docs/EXTENDING.md) describe the shared-module rule in place of the
libforge rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGAGAib517Ae1kg8SCdcEt
CI (ci.yml): the unit tier runs a fixed matrix of all twelve modules —
the seven services, smelt, its stress-tester, and the three shared
modules — on every change. It used to be path-filtered per service, which
was sound while no service module imported another and the shared surface
lived in an external library. With shared in-repo modules and
service-to-service dependencies, a path filter that misses a directory
skips CI silently and reports green; running everything costs minutes.
Two gates join: a replace-directive check (.github/scripts/check-replaces.sh
— every github.com/fil-forge/forge/* require in every go.mod must have a
replace pointing at the directory that holds that module; without one Go
resolves the path through the proxy to the repository root, which is not
a module) and the codegen gate for protocol and attestation
(make codegen-build gen-check). The stack tier builds all seven service
images from the commit under test and hands the satellites' to smelt
through DELEGATOR_IMAGE, SIGNER_IMAGE and INDEXER_IMAGE, so the full-stack
suites run against this commit's delegator, signer and indexer rather
than floating :main images.

docker/Dockerfile: the build context is the repository root — a service's
replace targets have to be inside the context. Every module's go.mod and
go.sum is copied before any source so dependency downloads stay cached,
and a BIN arg names the binary inside the image where it differs from the
directory (registrar, signer, indexer). The root .dockerignore keeps the
context small. Each service Makefile's image targets pass the root as the
context.

publish-ghcr.yml publishes all seven services on every merge, for the
same reason the unit tier stopped filtering. go.work lists every module.
The root Makefile's lists gain the satellites and the shared modules, plus
gen-check and check-replaces targets. README.md describes the layout.

Not changed here: release.yml still releases piri, hilt, sprue and ingot
only — whether the satellites are versioned and released from this
repository, and what happens to their deploy pipelines, is a decision
this branch does not make.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGAGAib517Ae1kg8SCdcEt
The stack tier's first run on this branch failed with "container
signing-service is unhealthy" while the signer's own log showed it up and
answering every probe: HEAD /healthcheck → 405 Method Not Allowed.

The probe is wget -q --spider. Under busybox wget — the alpine images the
satellites' own repositories publish, which smelt ran until this branch —
--spider sends a GET. Under GNU wget, which the shared debian image ships
(docker/Dockerfile), it sends a HEAD, and echo answers HEAD on a GET-only
route with 405, so wget exits 8 and Docker marks the container unhealthy.
The signer and the delegator both register their health route with
echo's GET; the indexer is on net/http's mux, whose GET patterns also
serve HEAD, so its probe happened to pass.

wget -q -O /dev/null is a GET under both wgets and still fails on any
non-2xx (exit 8), so it is the same check with the request method
pinned. All three satellite probes switch, the indexer's included so the
probe does not depend on a router detail. Reproduced against echo
v4.13.4 (the signer's version): --spider exits 8, -O /dev/null exits 0,
and -O /dev/null against a 404 exits 8.

The Dockerfile's note on the prod runtime records which wget it ships.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGAGAib517Ae1kg8SCdcEt
Peeja pushed a commit to fil-forge/forge-2 that referenced this pull request Sep 8, 2026
Changes to integrate the new w3infra compatible sprue implementation.
Peeja added a commit to fil-forge/forge-2 that referenced this pull request Sep 8, 2026
Peeja pushed a commit to fil-forge/forge-2 that referenced this pull request Sep 8, 2026
## Summary

Adds an `AGENTS.md` agent guide and points `CLAUDE.md` at it via the
`@AGENTS.md` convention. Written when the Forge workspace was
reorganized; landing it now that write access is available.

## Read the docs rendered

-
[AGENTS.md](https://github.com/fil-forge/delegator/blob/feat/agents-md-docs/AGENTS.md)

Generated with Claude Code
Peeja pushed a commit to fil-forge/forge-2 that referenced this pull request Sep 8, 2026
Adds a Ucanto server at `/` for getting your messages signed. You MUST have a delegation to `pdp/sign/*` from the signing service to use.

This PR is currently backwards compatible with the existing service (I haven't removed the old routes).

It also adds an `access/grant` handler so we don't need to ask the delegator for a delegation to use this service (Note: it is a TODO to actually validate the request).

BREAKING CHANGE: The client for this service is now a Ucanto server client. Old server routes are still available temporarily.

resolves storacha/piri-signing-service#2
Peeja added a commit to fil-forge/forge-2 that referenced this pull request Sep 8, 2026
* ci: add Dependabot for go modules and GitHub Actions

Weekly gomod + github-actions updates; part of the fil-forge CI standardization (pass 1: Dependabot org-wide).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HrVC6QSNKh9LM9LSFPkdUj

* ci: pin Unified CI reusables to a commit SHA (fil-forge#8)

Pin ipdxco/unified-github-workflows reusables from the floating @v1.0 tag to commit 63392e9 (v1.0). Part of the fil-forge CI standardization (pass 2); stacked on the Dependabot PR. Dependabot keeps the pin current.


Claude-Session: https://claude.ai/code/session_01HrVC6QSNKh9LM9LSFPkdUj

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants