Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#HILT_IMAGE=
#INGOT_IMAGE=
#PLC_IMAGE=
#SWARF_IMAGE=

# Forge Client
#GUPPY_IMAGE=
Expand Down
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ smelt/
│ ├── upload/ # Upload orchestration (mock w3infra)
│ ├── hilt/ # Tenant management (Fil One Tenant API + UCAN RPC)
│ ├── plc/ # did:plc directory (reference impl; hilt publishes tenant DIDs here)
│ ├── ingot/ # S3 facade (built from sibling ../ingot checkout)
│ ├── swarf/ # UCAN revocation service (+ its own postgres)
│ ├── guppy/ # CLI client
│ ├── ingot/ # S3 gateway over Forge (+ its own postgres)
│ ├── telemetry/ # Observability stack (present but not wired into Makefile)
Expand Down Expand Up @@ -317,6 +317,8 @@ All host-side ports live in a dedicated `15XXX` range to avoid collision with co
| plc-postgres | 15121 | PostgreSQL | did:plc directory store |
| ingot | 15130 | S3/HTTP | S3 gateway over Forge |
| ingot-postgres | 15131 | PostgreSQL | Ingot registry/metadata |
| swarf | 15140 | HTTP/UCAN | UCAN revocation service |
| swarf-postgres | 15141 | PostgreSQL | Swarf revocation store |
| guppy | (none) | CLI | Client container |

**Piri Shared Storage** (only emitted when at least one node uses that backend):
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ All host ports live in a dedicated `15XXX` range to avoid collision with common
| plc-postgres | 15121 | Postgres for the did:plc directory |
| ingot | 15130 | S3 gateway over Forge |
| ingot-postgres | 15131 | Postgres for ingot's registry/metadata |
| swarf | 15140 | UCAN revocation service |
| swarf-postgres | 15141 | Postgres for swarf's revocation store |
| guppy | — | CLI client for uploads (no exposed port) |

Piri's shared storage backends are generated from `smelt.yml` and only run when
Expand Down
1 change: 1 addition & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ include:
- path: systems/upload/compose.yml
- path: systems/guppy/compose.yml
- path: systems/plc/compose.yml
- path: systems/swarf/compose.yml
- path: systems/hilt/compose.yml
- path: systems/ingot/compose.yml

Expand Down
5 changes: 2 additions & 3 deletions embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,12 @@ import "embed"
//go:embed systems/signing-service/compose.yml
//go:embed systems/signing-service/config/*

//go:embed systems/swarf/compose.yml

//go:embed systems/upload/compose.yml
//go:embed systems/upload/config/*
//go:embed systems/upload/post_start.sh

//go:embed systems/ingot/compose.yml
//go:embed systems/ingot/config/*

// Curated snapshots shipped with the Go module so external consumers
// (importers of pkg/stack) can call stack.WithEmbeddedSnapshot without
// knowing anything about smelt's on-disk layout. New directories
Expand Down
1 change: 1 addition & 0 deletions pkg/generate/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var nonPiriServiceKeys = []string{
"guppy",
"hilt",
"ingot",
"swarf",
}

// GenerateKeys generates all cryptographic keys for the stack.
Expand Down
11 changes: 11 additions & 0 deletions pkg/stack/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type config struct {
blockchainImage string
ipniImage string
ingotImage string
swarfImage string

// Binary injection: bind-mount host-built binaries over the published
// images instead of rebuilding the image. serviceBinaries holds explicit
Expand Down Expand Up @@ -94,6 +95,9 @@ func (c *config) buildEnv() map[string]string {
if c.ingotImage != "" {
env["INGOT_IMAGE"] = c.ingotImage
}
if c.swarfImage != "" {
env["SWARF_IMAGE"] = c.swarfImage
}

return env
}
Expand Down Expand Up @@ -254,6 +258,13 @@ func WithIngotImage(image string) Option {
}
}

// WithSwarfImage sets the swarf (UCAN revocation) container image.
func WithSwarfImage(image string) Option {
return func(c *config) {
c.swarfImage = image
}
}

// WithTimeout sets the maximum time to wait for the stack to start.
func WithTimeout(d time.Duration) Option {
return func(c *config) {
Expand Down
18 changes: 18 additions & 0 deletions pkg/stack/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ func NewStack(ctx context.Context, t *testing.T, opts ...Option) (*Stack, error)
WaitForService("delegator", wait.ForHTTP("/healthcheck").WithPort("80/tcp").WithStartupTimeout(2*time.Minute)).
WaitForService("email", wait.ForHTTP("/api/server").WithPort("80/tcp").WithStartupTimeout(2*time.Minute)).
WaitForService("plc", wait.ForHTTP("/_health").WithPort("3000/tcp").WithStartupTimeout(2*time.Minute)).
WaitForService("swarf", wait.ForHTTP("/health").WithPort("80/tcp").WithStartupTimeout(2*time.Minute)).
WaitForService("hilt", wait.ForHTTP("/health").WithPort("80/tcp").WithStartupTimeout(2*time.Minute)).
WaitForService("ingot", wait.ForHTTP("/health").WithPort("9000/tcp").WithStartupTimeout(2*time.Minute))

Expand Down Expand Up @@ -442,6 +443,23 @@ func (s *Stack) IngotEndpoint() string {
return fmt.Sprintf("http://%s:%s", host, port.Port())
}

// SwarfEndpoint returns the HTTP endpoint for the swarf revocation service.
func (s *Stack) SwarfEndpoint() string {
container, err := s.compose.ServiceContainer(context.Background(), "swarf")
if err != nil {
s.t.Fatalf("getting swarf container: %v", err)
}
host, err := container.Host(context.Background())
if err != nil {
s.t.Fatalf("getting swarf host: %v", err)
}
port, err := container.MappedPort(context.Background(), "80/tcp")
if err != nil {
s.t.Fatalf("getting swarf port: %v", err)
}
return fmt.Sprintf("http://%s:%s", host, port.Port())
}

// maybeBinaryOverride builds workspace-selected service binaries (when enabled)
// and writes a compose override mounting them — plus any explicitly-provided
// binaries (WithServiceBinary / WithPiriBinary) — over the published images.
Expand Down
2 changes: 2 additions & 0 deletions pkg/stack/testmode_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ func testModeEnv(nodes []manifest.ResolvedPiriNode) map[string]string {
"SMELT_PLC_POSTGRES_PORT": "5432",
"SMELT_INGOT_PORT": "9000",
"SMELT_INGOT_POSTGRES_PORT": "5432",
"SMELT_SWARF_PORT": "80",
"SMELT_SWARF_POSTGRES_PORT": "5432",

// Piri shared infra — only used when any node declares postgres/s3,
// but harmless to set unconditionally (compose ignores unknown
Expand Down
1 change: 1 addition & 0 deletions pkg/workspace/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ var Services = map[string]serviceBuild{
"guppy": {moduleDir: "guppy", buildTarget: ".", binPath: "/usr/bin/guppy"},
"hilt": {moduleDir: "hilt", buildTarget: "./cmd/main.go", binPath: "/usr/bin/hilt"},
"ingot": {moduleDir: "ingot", buildTarget: "./cmd/ingot", binPath: "/usr/bin/ingot", configPath: "/etc/ingot/config.yaml"},
"swarf": {moduleDir: "swarf", buildTarget: "./cmd/swarf", binPath: "/usr/bin/swarf", configPath: "/etc/swarf/config.yaml"},
}

// libforgeDir is the workspace dir of the shared library. Its presence in the
Expand Down
3 changes: 3 additions & 0 deletions systems/hilt/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ services:
condition: service_healthy
plc:
condition: service_healthy
# Startup ordering only — hilt is not configured to call swarf yet
swarf:
condition: service_healthy
hilt-postgres:
condition: service_healthy
hilt-vault:
Expand Down
3 changes: 3 additions & 0 deletions systems/ingot/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ services:
condition: service_healthy
indexer:
condition: service_healthy
# Startup ordering only — ingot is not configured to call swarf yet
swarf:
condition: service_healthy
ingot-postgres:
condition: service_healthy
healthcheck:
Expand Down
69 changes: 69 additions & 0 deletions systems/swarf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Swarf (UCAN Revocation)

Swarf is the Forge network's UCAN revocation service. It accepts
`ucan/revoke` invocations over a UCAN RPC endpoint, stores revocation
records (revoked delegation CID + delegation path witness + cause
invocation) in Postgres, and serves them back over HTTP:

- `POST /` — UCAN RPC (`ucan/revoke`)
- `GET /revocation/:cid` — DAG-JSON revocation record (404 if absent)
- `GET /revocations/:since` — SSE firehose of revocation records
(`0` or an RFC3339 timestamp)
- `GET /.well-known/did.json` — did:web document
- `GET /health` — health status
- `GET /` — server info

## Services

- **swarf** - Revocation service (`ghcr.io/fil-forge/swarf:main`)
- **swarf-postgres** - PostgreSQL for swarf's revocation store (goose
migrations run at swarf startup)

## Ports

| Host Port | Container Port | Service | Description |
|-----------|----------------|---------|-------------|
| 15140 | 80 | swarf | UCAN RPC (`POST /`), revocation reads, did:web doc |
| 15141 | 5432 | swarf-postgres | PostgreSQL |

## Configuration

All configuration is via `SWARF_*` environment variables in `compose.yml`:

- Identity: `/keys/swarf.pem` wrapped as `did:web:swarf` (DID document
served at `/.well-known/did.json`).
- Storage: postgres via the `swarf-postgres` sidecar (dev-only
`swarf:swarf` credentials; data persists in the `swarf-postgres-data`
volume).
- `SWARF_SERVER_INSECURE_DID_RESOLUTION=true` so did:web documents
resolve over plain HTTP inside the compose network.
- did:plc directory: the local reference server at `http://plc:3000`
(`SWARF_PLC_DIRECTORY`), used to resolve did:plc issuers such as hilt
tenant identities.

## Keys

- `../../generated/keys/swarf.pem` - Swarf service identity (Ed25519)

## Volumes

- `swarf-postgres-data` - Swarf's revocation records

## Dependencies

- plc (service_healthy)
- swarf-postgres (service_healthy)

## Smoke Test

```bash
curl -sf http://localhost:15140/health # {"status":"healthy"}
curl -sf http://localhost:15140/ # server info banner
curl -s http://localhost:15140/.well-known/did.json | jq .id # "did:web:swarf"
curl -si http://localhost:15140/revocation/bafyreib3mqe6t2z3xwqcwoohw6f5o5t5nprfltmbjbmzynyqcnpxrcut4q # 404
```

## Used By

- Hilt and Ingot — startup ordering is wired (`depends_on`), but neither
is configured to call swarf yet.
71 changes: 71 additions & 0 deletions systems/swarf/compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Swarf System - UCAN revocation service
#
# Accepts ucan/revoke invocations over UCAN RPC, stores revocation records
# (revoked delegation CID + path witness + cause) in Postgres, and serves
# them back over HTTP plus an SSE firehose.
# Used by: hilt, ingot (config wiring pending)

services:
swarf:
image: ${SWARF_IMAGE:-ghcr.io/fil-forge/swarf:main}
user: "0:0" # Run as root for volume permissions (key files are 600)
ports:
# Internal port 80 for did:web resolution
- "${SMELT_SWARF_PORT:-15140:80}"
environment:
- SWARF_SERVER_HOST=0.0.0.0
- SWARF_SERVER_PORT=80
- SWARF_IDENTITY_KEY_FILE=/keys/swarf.pem
- SWARF_IDENTITY_SERVICE_ID=did:web:swarf
- SWARF_STORAGE_TYPE=postgres
# Dev-only creds, like hilt's hilt:hilt
- SWARF_STORAGE_POSTGRES_DSN=postgres://swarf:swarf@swarf-postgres:5432/swarf?sslmode=disable
# Resolve did:web documents over plain HTTP inside the compose network
- SWARF_SERVER_INSECURE_DID_RESOLUTION=true
# Resolve did:plc DIDs (e.g. hilt tenant identities) against the local
# PLC directory instead of the default https://plc.directory
- SWARF_PLC_DIRECTORY=http://plc:3000
- SWARF_LOG_LEVEL=info
volumes:
- ../../generated/keys/swarf.pem:/keys/swarf.pem:ro
depends_on:
plc:
condition: service_healthy
swarf-postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:80/health"]
start_interval: 1s
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: unless-stopped
networks:
- forge-network

# Postgres for swarf's revocation store. Swarf runs goose migrations on
# startup, so the `swarf` database created by POSTGRES_DB is enough.
swarf-postgres:
image: postgres:16-alpine
ports:
- "${SMELT_SWARF_POSTGRES_PORT:-15141:5432}"
environment:
- POSTGRES_USER=swarf
- POSTGRES_PASSWORD=swarf
- POSTGRES_DB=swarf
volumes:
- swarf-postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U swarf -d swarf"]
start_interval: 1s
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
restart: unless-stopped
networks:
- forge-network

volumes:
swarf-postgres-data:
3 changes: 3 additions & 0 deletions tests/e2e/smoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ func TestUploadAndRetrieve(t *testing.T) {
// Here we only assert the definition boots to healthy.
waitHTTPOK(t, s.IngotEndpoint()+"/health", 2*time.Minute)

// Same for swarf.
waitHTTPOK(t, s.SwarfEndpoint()+"/health", 2*time.Minute)

gup, err := guppy.NewContainerClient(s)
if err != nil {
t.Fatal(err)
Expand Down