Skip to content

Refactor/drop replaces - #3

Merged
frrist merged 14 commits into
mainfrom
refactor/drop-replaces
Jul 31, 2026
Merged

Refactor/drop replaces#3
frrist merged 14 commits into
mainfrom
refactor/drop-replaces

Conversation

@frrist

@frrist frrist commented Jul 31, 2026

Copy link
Copy Markdown
Member

Allows all modules to co-exist without replace statements by moving the shared seam of hilt and ingot to fil-forge/libforge#52 while also extracting integration tests from ingot to smelt - allowing repos to remain independent.

Base automatically changed from build/containers to main July 31, 2026 18:45
@frrist
frrist force-pushed the refactor/drop-replaces branch from 4677ccf to 4e87ca9 Compare July 31, 2026 18:45
frrist and others added 8 commits July 31, 2026 11:57
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>
…stead

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>
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>
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>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
@frrist
frrist force-pushed the refactor/drop-replaces branch from 4e87ca9 to 28f9272 Compare July 31, 2026 19:18
frrist and others added 6 commits July 31, 2026 13:16
…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>
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>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
@frrist
frrist merged commit 8d55284 into main Jul 31, 2026
11 of 12 checks passed
Peeja pushed a commit to fil-forge/forge-2 that referenced this pull request Sep 8, 2026
… (fil-forge#3) (#4) (fil-forge#5) (fil-forge#6) (fil-forge#8) (#9) (#10) (#11) (#12)

This PR introduces a new `pkg/store` layer with both in-memory and Postgres-backed implementations for core entities (provider, tenant, bucket, access key, delegation), plus embedded goose migrations and test utilities to run Postgres-backed store tests via testcontainers.

**Changes:**
- Add store interfaces + in-memory and Postgres implementations for provider/tenant/bucket/accesskey/delegation.
- Add embedded goose migrations and a migrations runner to initialize Postgres schema.
- Add test utilities and cross-implementation tests (memory + Postgres) for each store.

---

Adds a new Go client for interacting with the upload service via UCAN RPC invocations, along with structured logging helpers to make invocation arguments/metadata easier to inspect in logs.

**Changes:**
- Introduce `pkg/client.UploadClient` with `RegisterCustomer` (`/customer/add`) and `ProvisionSpace` (`/provider/add`) operations.
- Add `pkg/lib/zapucan` helpers for logging UCAN invocations and decoding CBOR-encoded IPLD maps into zap fields.
- Bump `github.com/fil-forge/libforge` dependency version.

---

Adds initial scaffolding for running hilt as a service: a Cobra-based `hilt serve` command that loads config via Viper and starts an Echo HTTP server wired together with Uber Fx.

**Changes:**
- Introduce Fx modules for config surfacing, zap logging, and Echo server lifecycle management.
- Add Viper-based config loading with defaults, env var support, and pflag binding.
- Add `cmd/main.go` CLI entrypoint plus basic Makefile/.gitignore and new dependencies (Echo, Cobra, Viper, Fx).

---

Adds initial Tenant + Access Key management API scaffolding and wires store backends (memory/postgres) into the Fx app based on configuration, including new config types/flags and Postgres migration hooks.

**Changes:**
- Introduces API route stubs and request/response types for tenant + access key endpoints, and auto-registers routes onto the Echo server via an Fx group.
- Adds storage backend selection (memory vs postgres) in `AppModule`, plus Fx modules to provide the corresponding store implementations.
- Expands configuration to include `storage.*` and `storage.postgres.*` settings and flag bindings; standardizes PG unique-violation handling using `pgerrcode`.

---

Adds a configurable “partner key” authentication mechanism to gate access to the Tenant API routes, wiring the key through config/flags and enforcing it via Echo middleware.

**Changes:**
- Introduces `AuthConfig` (`auth.partner_key`) and exposes it through the fx config module.
- Wraps all Tenant API routes in partner-key bearer auth middleware while keeping `/` and `/health` open.
- Adds new Echo middleware + unit tests for partner-key bearer authentication.

---

Adds a KMS-agnostic `vault.Vault` interface plus in-memory and HashiCorp Vault (KV v2) implementations, and wires vault selection/config into the fx app and CLI so Hilt can persist private key material in a configurable backend.

**Changes:**
- Introduce `pkg/vault.Vault` interface with `ErrNotFound`, plus an in-memory implementation and a HashiCorp Vault (KV v2) implementation (including AppRole auth helper).
- Add config schema + viper defaults + CLI flags for selecting/configuring the vault backend, and wire backend selection into the fx app graph.
- Add tests for the vault interface contract and HashiCorp AppRole login using a testcontainers Vault dev container.

---

Implements tenant provisioning via `PUT /tenants/{tenantId}` by introducing an external tenant identifier, wiring a did:plc directory client, generating/storing tenant keys in Vault, and persisting tenant records.

**Changes:**
- Added `external_id` to tenant storage model and introduced `GetByExternalID` for idempotent provisioning.
- Added PLC directory configuration + fx wiring to provide a `plc.DirectoryClient`.
- Implemented `ProvisionTenant` API handler with accompanying tests (memory stores + httptest PLC server).

---

Implements the remaining tenant management endpoints by wiring real store-backed handlers and adding the store capabilities required to support tenant deletion cascades (buckets, access keys, delegations, vault secrets, and did:plc deactivation).

**Changes:**
- Implement `GET /tenants/:tenantId`, `POST /tenants/:tenantId/status`, and `DELETE /tenants/:tenantId` handlers (with did:plc deactivation + cascade cleanup).
- Extend tenant/bucket/access-key stores with `Delete` and “list by tenant” capabilities, including pagination for buckets.
- Add/extend unit tests for the new store methods and HTTP handlers; bump dependencies to newer `ucantone` and `dag-json-gen`.

---

This PR implements the Management API access-key endpoints, including key material storage in Vault and issuing tenant→access-key UCAN delegations based on requested S3 permissions. It also extends the store layer to support access-key expiry and efficient bucket lookups by ID for response rendering.

**Changes:**
- Implement Create/List/Get/Delete access-key handlers, including Vault secret storage and delegation issuance/revocation.
- Add `expires_at` support for access keys across schema, store implementations, and tests.
- Extend bucket listing to support filtering by explicit bucket IDs via new `bucket.ListOption`/`bucket.ListConfig`.

---

Adds initial scaffolding for Hilt’s UCAN-based RPC surface (used by Ingot for S3 tenant management), including an identity-backed UCAN server mounted on the existing Echo HTTP server.

**Changes:**
- Introduces UCAN RPC handler stubs and wires them into an `fx` module that builds a `ucantone` HTTP server.
- Adds a configurable service identity (PEM-backed or ephemeral) and exposes a public `did:web` DID document endpoint.
- Extends configuration/CLI flags to support identity settings.
Peeja pushed a commit to fil-forge/forge-2 that referenced this pull request Sep 8, 2026
- Link directly to the relevant RFC sections for easy checking: RFC 3394
  §2.2.3.1 (default IV) and §4 (test vectors), plus RFC 3394/5649 references
  in the package doc.
- Document that only the base RFC 3394 mode is supported (single fixed initial
  value), not the RFC 5649 padded variant / Alternative Initial Value, which is
  what would allow non-block-aligned key-data lengths.
- Name the RFC 3394 round count as a `rounds` constant rather than a literal 6.

No behavior change; aeskw and wrap tests still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fzupq5mPYzBGqkbUGabJyP
Peeja pushed a commit to fil-forge/forge-2 that referenced this pull request Sep 8, 2026
Review fact-check on PR fil-forge#3:
- The Alternative Initial Value is introduced by RFC 3394 itself (§2.2.3.2),
  which anticipates IVs for key data that "may not always be a multiple of 64
  bits" and defers concrete definitions to future work; RFC 5649 is one such,
  defining an AIV with a message-length indicator for padded wrapping. Correct
  the package doc, which had credited RFC 5649 with introducing the AIV.
- Add a comment noting the nested loop performs rounds*n = 6n block encryptions
  total, matching the RFC's single-index t = 1..6n form (answering a review
  question about the loop bound; behavior unchanged, RFC 3394 §4 vectors pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fzupq5mPYzBGqkbUGabJyP
Peeja pushed a commit to fil-forge/forge-2 that referenced this pull request Sep 8, 2026
Per review on PR fil-forge#3: both this package and aeskw implement "wrapping", so the
bare name `wrap` was ambiguous. Rename fee/wrap -> fee/ecdhkw ("ECDH key wrap",
parallel to aeskw = "AES key wrap"); wrap.go -> ecdhkw.go and wrap_test.go ->
ecdhkw_test.go. The exported API (Wrap, Unwrap, Wrapped, AlgorithmECDHESA256KW)
is unchanged.

Also apply the no-leading-colon error convention here, matching aeskw: messages
carry the bare package name as a prefix (e.g. "ecdhkw nil recipient public
key"), reserving ": %w" for genuine error wrapping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fzupq5mPYzBGqkbUGabJyP
Peeja added a commit to fil-forge/forge-2 that referenced this pull request Sep 8, 2026
…256kw-tenant-recipient-wrapunwrap-over-x25519

fee/ecdhkw: ECDH-ES+A256KW tenant recipient wrap/unwrap over X25519 (FIL-471)
Peeja pushed a commit to fil-forge/forge-2 that referenced this pull request Sep 8, 2026
* wip: migrating to ucan1 service

* fix: generate ID for guppy and configure in container

* smoll handoff doc

* fix: guppy data dir

* feat: go.work-based local dev (SMELT_WORKSPACE) replacing SMELT_DEV hack

Build services from local sibling checkouts via a Go workspace + binary
injection, replacing the branch-local SMELT_DEV / compose.dev.yml / forked
Dockerfile approach (which also silently ignored local libforge edits).

- pkg/workspace: detect services from the active go.work use-list, compile
  each from local source (static linux/amd64), and render a compose override
  that bind-mounts the binaries over the published images. libforge in the
  use-list forces all services to rebuild.
- pkg/stack: WithWorkspaceBinaries() + WithServiceBinary(); the piri-only
  binary override is generalized to all services.
- cmd/smelt: `workspace build` subcommand; Makefile SMELT_WORKSPACE=1 parity
  for make up/build/fresh via generated/compose/workspace.override.yml.
- tests/e2e: opt into workspace binaries when SMELT_WORKSPACE=1.
- Remove compose.dev.yml, dockerfiles/, and the generator's dev-mode code.
- docs: add docs/DEVELOPING.md; update CLAUDE.md/README/EXTENDING

* fix: generate ucan1 proofs via ucantone/libforge; pin workspace build toolchain

The Go test stack generated delegation proofs with go-ucanto (ucan 0.9), but
the ucan1 services decode them with ucantone, so the delegator failed at startup
with "unmarshaling delegation envelope CBOR: cbor input should be of type array"
(while `make` worked, since it shells out to `delegator gen`).

- pkg/stack/proofs.go: mirror delegator/cmd/gen.go — build delegations with
  libforge command primitives (claim.Cache / egress.Track), subject = issuer's
  own DID, and serialize with ucantone delegation.Encode. Drop the per-node
  piri->upload proofs (obsolete in ucan1; provider registration no longer reads
  proof files). Adds ucantone + libforge, drops go-ucanto.
- pkg/workspace: build workspace binaries with the test's own toolchain
  (runtime.GOROOT()/bin/go) rather than bare PATH `go`, so a stale system go on
  an IDE run-config PATH (e.g. GoLand's /usr/bin/go 1.18) can't break the build.
- tests/e2e/smoke_test.go: run against workspace-built binaries unconditionally.

* feat: switch mkdelegation to ucantool

* regenerate snapshot

* address feedback

* make linter happy

---------

Co-authored-by: Alan Shaw <alan138@gmail.com>
Peeja pushed a commit to fil-forge/forge-2 that referenced this pull request Sep 8, 2026
This is part of reverting the change to send allocate and accept delegations from the client. We decided this should in fact be made with the storage provider as the subject and that the upload service should be pre-authorized by the storage provider to invoke.

This PR updates the delegator to expect proofs to be provided by Piri so that the upload service can invoke `/blob/allocate`, `/blob/accept`, `/blob/replica/allocate` and `/pdp/info`.

It also adds validation that the required delegations were provided.
Peeja pushed a commit to fil-forge/forge-2 that referenced this pull request Sep 8, 2026
…il-forge#3)

* refactor: migrate signing-service to ucantone + libforge (UCAN 1.0)

Server side
- pkg/server/server.go is now ~30 lines: ucantone server.NewHTTP +
  bindexec.NewHandler wrappers for the five capabilities. The old
  go-ucanto Option chain + WithServiceMethod / Provide pattern is gone.
- Handlers (pkg/server/handlers/{access_grant,sign}.go) move to
  bindexec.HandlerFunc[A, O]. Args come from req.Task().Arguments();
  subject validation is "task.Subject() != id.DID()". The
  /access/grant handler issues a delegation per requested capability
  (UCAN 1.0 delegations are single-command) and attaches the signed
  envelopes via res.SetMetadata(container.New(WithDelegations(...))).
- pkg/server/handlers/sign.go translates eip712.AuthSignature (fixed-
  size address/hash fields) into libforge sign.AuthSignature (raw
  []byte fields) for the receipt OK.

Client side
- pkg/client/client.go uses ucantone client.HTTPClient and libforge
  capability bindings. Each method takes proofs []ucan.Delegation so
  callers can attach a /access/grant delegation; their CIDs ride on
  the invocation, the envelopes ride on the execution request.
- pkg/client/client_test.go wires a ucantone server as an http.Round
  Tripper for the client and exercises the full encode/decode path
  without listening on a port.

Inprocess + types
- pkg/types.SigningService picks up the new proofs []ucan.Delegation
  parameter; in-process implementation accepts and ignores it
  (authorization is out-of-band).
- pkg/config switches the service signer to ucantone ed25519/signer/
  did Parse + Wrap.
- main.go wires the ucantone HTTPServer into Echo via echo.WrapHandler;
  the legacy POST / handler path that streamed UCAN responses through
  the go-ucanto transport is gone.

* rest of the changes for piri

* deps: update ucantone and libforge

* fix: pass 32-byte seed to ucantone ed25519.FromRaw

SignerFromEd25519PEMFile parses a PKCS#8 PEM into a crypto/ed25519
PrivateKey, which is 64 bytes (seed||pub). ucantone's ed25519.FromRaw
expects exactly 32 bytes (the seed). Without .Seed(), startup fails
with "loading service key from PEM file: invalid length: 64 wanted: 32".

* needs did resolution

* update to latest libforge

* update latest ucantone

* feat: make did:web resolution configurable

- supporting secure and insecure resolution

* refactor: remove unused log var
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