Skip to content

feat: adopt Hilt's S3 client and wire contract from the forge monorepo - #52

Draft
frrist wants to merge 1 commit into
mainfrom
feat/hilt-s3-client
Draft

feat: adopt Hilt's S3 client and wire contract from the forge monorepo#52
frrist wants to merge 1 commit into
mainfrom
feat/hilt-s3-client

Conversation

@frrist

@frrist frrist commented Jul 31, 2026

Copy link
Copy Markdown
Member

Ingot (and any future S3 gateway) consumes Hilt through a client and a wire contract that until now lived inside the hilt module, forcing ingot to carry a replace directive that breaks external consumers of the ingot library. This makes libforge their home, next to the commands/s3 bindings they are built from:

  • client/hilt: the Hilt S3 UCAN RPC client (Client, options)
  • commands/s3: Operation + OperationFor (request classification)
  • commands/s3/request, commands/s3/bucket: named error contract
  • sigv4: stdlib-only SigV4/SigV4a verification and key derivation
  • s3perm: S3 permission -> Forge command mapping
  • ucan/zapucan: zap logging helpers for ucan values

Also bumps ucantone to the version the forge services already unified on.

Ingot (and any future S3 gateway) consumes Hilt through a client and a wire
contract that until now lived inside the hilt module, forcing ingot to carry
a replace directive that breaks external consumers of the ingot library.
This makes libforge their home, next to the commands/s3 bindings they are
built from:

- client/hilt: the Hilt S3 UCAN RPC client (Client, options)
- commands/s3: Operation + OperationFor (request classification)
- commands/s3/request, commands/s3/bucket: named error contract
- sigv4: stdlib-only SigV4/SigV4a verification and key derivation
- s3perm: S3 permission -> Forge command mapping
- ucan/zapucan: zap logging helpers for ucan values

Also bumps ucantone to the version the forge services already unified on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
frrist added a commit to fil-forge/forge that referenced this pull request Jul 31, 2026
All six modules (hilt, ingot, ingot/itest, piri, smelt, sprue) now require
libforge v0.0.0-20260731172903-928cf2a21b7e — the fil-forge/libforge#52 head —
so the whole repo agrees on one libforge again, and hilt/ingot resolve the
extracted client/contract packages without any workspace. hilt and ingot
container images build standalone for the first time since the extraction.

The bump crosses libforge's breaking change #50 (blob-release Cause), which
this commit adopts:

- sprue: /blob/release now carries Cause — the task link of the client
  /blob/remove it translates — and ships the remove invocation in the request
  container so the node can verify the release's origin. The remove handler
  threads both through forwardBlobRelease.
- piri: release-handler tests construct wire-valid arguments (Cause set,
  following the RandomCID precedent the allocate tests use). The node-side
  verification #50 calls for (reject UnknownCause/InvalidCause) is not yet
  implemented — piri still accepts releases without checking the cause chain.

After PR#52 merges, re-pin to the merged commit (or the next libforge tag) so
go.mod references a commit on main rather than a PR head.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
frrist added a commit to fil-forge/forge that referenced this pull request Jul 31, 2026
All six modules (hilt, ingot, ingot/itest, piri, smelt, sprue) now require
libforge v0.0.0-20260731172903-928cf2a21b7e — the fil-forge/libforge#52 head —
so the whole repo agrees on one libforge again, and hilt/ingot resolve the
extracted client/contract packages without any workspace. hilt and ingot
container images build standalone for the first time since the extraction.

The bump crosses libforge's breaking change #50 (blob-release Cause), which
this commit adopts:

- sprue: /blob/release now carries Cause — the task link of the client
  /blob/remove it translates — and ships the remove invocation in the request
  container so the node can verify the release's origin. The remove handler
  threads both through forwardBlobRelease.
- piri: release-handler tests construct wire-valid arguments (Cause set,
  following the RandomCID precedent the allocate tests use). The node-side
  verification #50 calls for (reject UnknownCause/InvalidCause) is not yet
  implemented — piri still accepts releases without checking the cause chain.

After PR#52 merges, re-pin to the merged commit (or the next libforge tag) so
go.mod references a commit on main rather than a PR head.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
frrist added a commit to fil-forge/forge that referenced this pull request Jul 31, 2026
* refactor(hilt): consume the S3 client and wire contract from libforge

The Hilt S3 RPC client, sigv4, s3perm, zapucan, and the /s3/* wire contract
(named error sentinels, Operation/ClassifyRequest) moved to libforge, next to
the commands/s3 bindings they are built from. Hilt keeps the server side:
the Authorizer and bucket services, stores, vault, plus the hilt-internal
AdminClient and Sprue UploadClient.

This is the first half of removing ingot's replace directives: with the
contract in libforge, ingot no longer needs to import the hilt module at all.

hilt/go.mod still pins the pre-move libforge; it is bumped when the libforge
change lands (until then, builds resolve via the workspace).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(ingot): drop the hilt replace directive, consume libforge instead

ingot's hilt surface (client, sigv4, s3perm, auth/bucket wire contract) now
lives in libforge, so the require + replace of the hilt module goes away.
This is half of what makes the ingot library consumable outside the repo:
a downstream 'go get' no longer resolves a nonexistent hilt@v0.0.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(ingot): split itest into a nested test-only module

itest imports smelt's stack harness, and go.mod cannot mark a dependency as
test-only — so as long as itest lived in the library module, ingot required
the unpublished smelt module and needed a replace directive to resolve it.

itest/ is now its own leaf module. Nothing imports a test module, so its two
replace directives (ingot, smelt) are harmless — they never appear in any
consumer's build. ingot's library go.mod is left with no intra-repo
requires at all: with the hilt surface consumed from libforge (previous
commit), both replace directives are gone and the library resolves like any
published module.

The itest module is added to go.work, and 'make itest' now enters itest/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* build: return every container build to its own service directory

With ingot's replace directives gone, no Dockerfile needs a sibling module in
its build context. Revert the repo-root-context machinery from the container
work: piri/hilt/sprue Dockerfiles go back to their service-directory form,
ingot's drops the hilt/smelt manifest and source copies, publish-ghcr.yml
builds each service with context <service>/, and ingot no longer rebuilds on
hilt or smelt changes. The root .dockerignore is gone (nothing builds from
the root); each service's own .dockerignore applies again, now also ignoring
goreleaser's dist/.

smelt's BuildImage helpers revert to their one-directory-per-service shape:
BuildPiriImage(t, "../piri") builds piri from its own directory, same as the
out-of-repo services.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(ingot): match CLAUDE.md to the tags publish-ghcr actually pushes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* deps: pin libforge at the PR#52 commit across all services

All six modules (hilt, ingot, ingot/itest, piri, smelt, sprue) now require
libforge v0.0.0-20260731172903-928cf2a21b7e — the fil-forge/libforge#52 head —
so the whole repo agrees on one libforge again, and hilt/ingot resolve the
extracted client/contract packages without any workspace. hilt and ingot
container images build standalone for the first time since the extraction.

The bump crosses libforge's breaking change #50 (blob-release Cause), which
this commit adopts:

- sprue: /blob/release now carries Cause — the task link of the client
  /blob/remove it translates — and ships the remove invocation in the request
  container so the node can verify the release's origin. The remove handler
  threads both through forwardBlobRelease.
- piri: release-handler tests construct wire-valid arguments (Cause set,
  following the RandomCID precedent the allocate tests use). The node-side
  verification #50 calls for (reject UnknownCause/InvalidCause) is not yet
  implemented — piri still accepts releases without checking the cause chain.

After PR#52 merges, re-pin to the merged commit (or the next libforge tag) so
go.mod references a commit on main rather than a PR head.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: move the S3 system suite and its glue from ingot into smelt

The itest suite never tested ingot-the-module — it boots the whole Forge
stack and drives it through ingot's S3 API, with the working-tree ingot
binary mounted by the harness. It is a system suite, and it now lives with
the system: the S3-client glue (former ingot/testing) becomes
smelt/pkg/s3glue, and the conformance partition + scenarios (former
ingot/itest) become smelt/tests/s3, inside smelt's own module next to where
the compat suites are headed.

This deletes the nested ingot/itest module and with it the last replace
directives in the repository. Both of its imports (pkg/stack, the glue) are
now same-module, so there is nothing left to resolve by path: every go.mod
in the repo is resolvable by any consumer, and in-repo HEAD-vs-HEAD testing
is preserved because tests and harness share a module and the service under
test arrives as a working-tree binary, not an import.

Cost, accepted deliberately: smelt's module graph gains aws-sdk-go-v2 and
the versitygw fork (go.mod cannot mark test-only deps). smelt's consumers
are themselves test suites, so graph leanness buys them nothing.

ingot's 'make itest' forwards to smelt's new 'make test-s3'; docs updated to
the new division: out-of-repo services keep behavior tests in their repos
(pseudo-version imports of pkg/stack), in-repo system suites live in
smelt/tests/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: unit tier per module + one full-stack run per PR

Two tiers over one commit, mirroring the old per-repo split:

  unit   per-module, path-filtered, GOWORK=off, CGO_ENABLED=0 (piri adds
         -tags=skiff). Each filter is just the service's own subtree — no
         module imports another, so nothing else can break its compile.
  stack  one run per PR: smelt's e2e smoke suite, then the S3 gateway system
         suite (smelt/tests/s3), which compiles the working tree's ingot and
         bind-mounts it over the published image. Sequential in one job: the
         suites sweep smeltery-* containers, so two stacks must never share
         a Docker host. Container logs are dumped and uploaded on failure.

Also fixes a latent path bug from the itest move: localIngotBinary built
from '..' (correct when the suite lived at ingot/itest, wrong from
smelt/tests/s3). Verified end-to-end by running TestForgeNativeProvision
against a real stack boot: working-tree binary mounted, hilt tenant
provisioned, 512KiB round-trip through sprue/piri.

The stack job pulls ghcr.io/fil-forge/forge/<svc>:main for the sibling
services, so it needs the container-build changes merged to main (which
publishes those tags) before it can go green on a PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: remove failing piri (sqlite) smoke tests

* test(smelt): skip the snapshot restore e2e until a postgres snapshot exists

The only committed snapshot (3-piri-filesystem-sqlite) captures a
sqlite-backed piri, and piri's curio PDP pipeline now refuses sqlite —
the fixture can never boot healthy again. Skip with the unskip condition
stated: capture and commit a postgres-backed snapshot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: serialize e2e subtests — one stack per Docker host

Each e2e subtest boots a complete Forge stack. Without -parallel 1 Go runs
them GOMAXPROCS-wide, and two simultaneous stack boots overwhelm the 2-vCPU
runner: a container exec misses its readiness handshake and compose fails
with runc's 'procReady not received'. Same guard ci/stack's ci.yml already
carries; SMELT_E2E_MAX_PARALLEL stays as the escape hatch for beefier hosts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: name the unit steps so the job view reads as a checklist

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* build: one Go version across every module — 1.26.5

The six modules had drifted across five different go directives (1.24.0 to
1.26.4), which just failed PR CI in a subtle way: the stack job installs the
toolchain from smelt/go.mod (1.25.7), setup-go pins GOTOOLCHAIN=local, and
the S3 suite's in-test 'go build ./cmd/ingot' then refuses — ingot requires
>= 1.26.4 and auto-download is disabled.

All modules now declare go 1.26.5 (the latest release, and what go.work and
the shared builder image already use), so whichever go.mod a workflow reads,
the installed toolchain satisfies every module. Same rationale as the
dependency unification: one commit should hold one view of the toolchain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(smelt): grant the conformance key hilt's multipart permissions

The stack tier's whole point — every service at this commit — surfaced a
drift the floating-baseline runs had been masking: hilt now classifies
multipart operations distinctly and requires s3:AbortMultipartUpload,
s3:ListMultipartUploadParts and s3:ListBucketMultipartUploads, but the test
tenant's access key list predated them. Hilt rejected those operations as
OperationNotPermitted before bucket/argument validation, so pass-table cases
expecting NoSuchBucket / InvalidArgument saw AccessDenied.

Adds the three permissions and a lockstep guard
(TestHiltPermissionsCoverS3Perm): every listed permission must be one s3perm
recognizes, and every classified operation's required permission must be in
the list — so the key can never silently lag the permission model again.
Verified against a live stack: the ListMultipartUploads, AbortMultipartUpload
and CompleteMultipartUpload categories all pass.

Also bumps sprue's (branch-local, soon-deleted) Dockerfile builder to
golang:1.26 — the go 1.26.5 unification made the 1.25 base unable to compile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <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.

1 participant