Skip to content

FEE wrap material on blob_encryption_params (FIL-480) - #15

Merged
alanshaw merged 11 commits into
mainfrom
claude/fil-480-cipgkz
Aug 24, 2026
Merged

FEE wrap material on blob_encryption_params (FIL-480)#15
alanshaw merged 11 commits into
mainfrom
claude/fil-480-cipgkz

Conversation

@Peeja

@Peeja Peeja commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

tl;dr: This adds everything Ingot needs to do the read-path decryption. It stores a wrapped CEK, as well as other info that's in the FEE header that's needed for decryption. That means that Ingot can perform a range read by grabbing just the bytes it needs to decrypt (rounded up to encryption chunk boundaries), and not also fetch the header, which is likely not contiguous.

It's all stored on ingot.blob_locations because that feels like the right entity shape-wise, but the name may no longer be appropriate. @frrist, should we broaden the name of this table?

See #15 (comment) for what changed since the description below.


Summary

Implements FIL-480. Extends the existing ingot.blob_locations table with the per-blob FEE (FilOne encryption envelope) wrap material that the read path needs to decrypt an encrypted blob — so a (range) GET can unwrap the CEK and go straight to a body-range fetch, with no COSE envelope-header round-trip.

Previously stacked on #9 (feat/arch-migration-phase1). #9 has merged, so this is now rebased onto main and targets main; only the single FIL-480 commit remains.

Why extend blob_locations rather than add a table

An earlier draft of the issue proposed a new object_encryption / per-segment table with plaintext_size / plaintext_etag / a generation counter. On review those turned out redundant, and the issue was rewritten to an extension:

  • Object-level values already exist. ObjectManifest carries ETag and Body.Size/MD5/SHA256 — properties of the plaintext, computed pre-encryption in the PUT pipeline (FIL-481). No new columns.
  • No generation counter. Overwrite already swaps the MST leaf to a new manifest CID atomically; a reader resolves one manifest or the other, never a partial mix.
  • The wrap material is a 1:1 fact about a (space, digest) row. A fresh CEK per encryption event makes every ciphertext digest unique to one encryption (never shared across objects, even for identical plaintext) — exactly like the existing provider/url/size columns. No region column either: one Ingot instance is one region, so region_key_version alone identifies the KEK version.

What changed

  • migrations/sql/00004_blob_encryption.sql — additive ALTER TABLE ingot.blob_locations ADD COLUMN for six nullable columns (region_wrapped_cek, region_key_version, tenant_recipient_kid, base_nonce, chunk_size, protected_header), plus a Down. An unencrypted blob (the appliance topology today) leaves them NULL.
  • registry.BlobLocation gains the six fields (documented); PutLocation/GetLocation carry them (nullString/nullInt64 for optional text/bigint, bytea via nil). The upsert overwrites full row state, so a rotation re-wrap is a read-modify-write.
  • inmem.MemStore deep-copies the new byte-slice fields so stored/returned copies don't alias.
  • Tests — inmem FEE round-trip + slice-aliasing safety + re-wrap-in-place; live-Postgres bytea/NULL round-trip, re-wrap, and the unencrypted (all-NULL) case; migration column existence + nullability.

Security / acceptance

  • Raw CEK bytes are never stored — only the region-KEK-wrapped CEK plus opaque key-version / recipient identifiers.
  • region_key_version / tenant_recipient_kid are opaque, so no schema change is needed if the region-key or Hilt wrap-key cardinality decisions (FIL-572 / FIL-574) later go multi-key.
  • Per-blob crypto-shred = nulling these columns (or DeleteLocation); no separate mechanism. Worth confirming against FIL-489's reference-counted delete.

Testing

  • GOWORK=off go build ./... ✅ · go vet ./...
  • go test ./... passes for all packages except the pre-existing, environment-only failure in ./testing/ (unable to add custom RootCAs HTTPClient … — reproduced identically on main with none of these changes; it's the sandbox's HTTPS-proxy CA setup, not this diff).
  • The live Postgres/migration tests are gated behind INGOT_TEST_DSN and were not run here (no DB in this sandbox). Please run them in CI/local:
    INGOT_TEST_DSN=postgres://postgres:pw@127.0.0.1:55432/ingot \
      GOWORK=off go test ./registry/ ./migrations/ -run Live -v
    

Open question for reviewers (non-blocking)

blob_locations's doc comment described it narrowly as the (space, digest) → provider/URL mapping. Adding encryption material broadens its role to "the full per-blob record." I've updated the doc comments to reflect that but left the table name as-is. Should it be renamed (e.g. blob_records)? Flagging per the issue — happy to do the rename in a follow-up if we want it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends the local ingot.blob_locations record shape to include the per-blob FEE (FilOne encryption envelope) wrap material needed for range reads to decrypt encrypted blobs without an extra envelope-header fetch, along with SQL migration and store/test updates.

Changes:

  • Adds a new SQL migration to extend ingot.blob_locations with six nullable FEE wrap-material columns (plus Down migration).
  • Extends registry.BlobLocation and updates the Postgres/inmem LocationStore implementations to read/write these new fields (including deep-copying byte slices in-memory).
  • Adds/extends live-Postgres + migration + inmem tests to validate NULL/bytea round-trips, aliasing safety, and re-wrap updates.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
registry/stores.go Expands BlobLocation to carry FEE wrap material and updates doc comments/contracts.
registry/stores_postgres.go Updates PutLocation/GetLocation SQL to write/read new nullable columns and adds nullInt64.
registry/postgres_live_test.go Adds live-Postgres tests for NULL semantics, bytea round-trips, and re-wrap-in-place behavior.
migrations/up_live_test.go Verifies new columns exist and are nullable after migration.
migrations/sql/00004_blob_encryption.sql Adds the actual schema migration for FEE wrap material columns on blob_locations.
inmem/stores.go Ensures in-memory store deep-copies new byte-slice fields to avoid aliasing.
inmem/stores_test.go Adds inmem tests for wrap-material round-trip, aliasing safety, and re-wrap-in-place behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread registry/stores_postgres.go
@frrist
frrist force-pushed the feat/arch-migration-phase1 branch from be552e3 to a59b01d Compare July 1, 2026 23:21
Extend ingot.blob_locations with the FEE encryption columns a (range) GET
needs to decrypt an encrypted blob without fetching the COSE envelope header:
region_wrapped_cek, region_key_version, tenant_recipient_kid, base_nonce,
chunk_size, and protected_header. All nullable — an unencrypted blob (the
appliance topology today) leaves them NULL.

Rather than a new object/segment table, these live on blob_locations: it is
already keyed by (space, digest), and a fresh CEK per encryption event makes
each ciphertext digest unique to one encryption, so the wrap material is a 1:1
fact about the row — like the existing provider/url/size. Object-level values
(plaintext size/ETag/identity) stay in the content-addressed ObjectManifest;
overwrite already swaps the MST leaf to a new manifest CID atomically, so no
plaintext_* columns and no generation counter are needed.

Raw CEK bytes are never stored — only the region-KEK-wrapped CEK plus opaque
key-version/recipient identifiers, so no schema change is needed if the
region-key or Hilt wrap-key cardinality decisions (FIL-572/FIL-574) later go
multi-key. Per-blob crypto-shred is nulling these columns or deleting the row;
a rotation re-wrap is a PutLocation with a new wrapped CEK + key version.

- migration 00004: additive ALTER TABLE ADD COLUMN (+ Down)
- registry.BlobLocation gains the six fields; Put/GetLocation carry them
  (nullString/nullInt64 for optional text/bigint, bytea via nil)
- inmem MemStore deep-copies the new byte-slice fields
- tests: inmem FEE round-trip + mutation-safety; live-PG bytea/NULL round-trip
  and re-wrap-in-place; migration column existence + nullability

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XSxwh7XfPtFQtyeoDR65hC
@Peeja
Peeja force-pushed the claude/fil-480-cipgkz branch from bc83565 to e054d52 Compare July 2, 2026 17:54
@Peeja
Peeja changed the base branch from feat/arch-migration-phase1 to main July 2, 2026 17:54
Peeja and others added 3 commits July 2, 2026 13:59
…IL-480)

Addresses a PR review note: BlobLocation documents the FEE wrap fields as
all-or-nothing, but PutLocation accepted a partial set (e.g. a wrapped CEK
with no nonce, or an empty-but-non-nil byte slice stored as a non-NULL empty
bytea), silently persisting a row the decrypt path cannot use.

- BlobLocation.ValidateFEE enforces the invariant: either all six wrap fields
  are present (non-empty, chunk_size > 0) or none; a partial set returns
  ErrPartialFEE. Both PutLocation implementations (Postgres + inmem) call it.
- nullBytes maps empty/nil byte slices to SQL NULL so an absent FEE column
  never lands as a non-NULL empty bytea.
- tests: partial sets rejected (and leave no row) in the inmem and live-PG
  suites; the fully-absent (unencrypted) row is still accepted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XSxwh7XfPtFQtyeoDR65hC
go-check flagged inmem/stores_test.go as not gofmt-ed: the TestLocations_
PartialFEE_Rejected map keys were aligned against a multi-line entry. Run
gofmt -w. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XSxwh7XfPtFQtyeoDR65hC
@Peeja
Peeja marked this pull request as ready for review July 2, 2026 19:58
@Peeja
Peeja requested review from alanshaw, bajtos, frrist and pyropy July 2, 2026 19:58
Comment thread registry/stores.go
@bajtos bajtos self-assigned this Jul 30, 2026
@bajtos
bajtos requested a review from Copilot August 3, 2026 09:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread registry/stores_postgres.go Outdated

@bajtos bajtos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code looks good to me 👍🏻

However, I strongly dislike that we are adding non-location fields to blob_locations table. Let's rename the table and interfaces (e.g. PutLocation) to use a more appropriate name, like BlobRecord or BlobMetadata.

Comment thread migrations/sql/00013_blob_encryption.sql Outdated
Comment thread migrations/sql/00013_blob_encryption.sql Outdated
Comment thread migrations/sql/00013_blob_encryption.sql Outdated
Comment thread migrations/sql/00013_blob_encryption.sql Outdated
Comment thread migrations/sql/00013_blob_encryption.sql Outdated
bajtos added 2 commits August 17, 2026 14:15
Two textual conflicts (registry/stores.go imports, appended subtests in
registry/postgres_live_test.go) plus two the merge resolved cleanly but
would not have compiled:

- main retyped the space column as did.DID (a struct); the FEE tests
  still passed string literals. Replaced with testutil.RandomDID.
- main's migrations reached 00012, colliding with this branch's 00004
  blob-encryption migration. Renumbered it to 00013; it only adds
  nullable columns, so running last is equivalent.

Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Assisted-by: Claude:claude-opus-5
Review of the FIL-480 draft rejected extending blob_locations with the
per-blob FEE wrap material. blob_locations is a reconstructible cache of
the indexing-service contract — every row can be re-derived from the
indexer or the accept receipt, and the table disappears when the topology
moves to a real indexer. A wrapped CEK is not reconstructible: lose the
row and the ciphertext is unreadable forever. Key material therefore does
not belong in the one table whose design allows it to be rebuilt, or
truncated (the live test already does).

blob_encryption_params holds it instead, with no foreign key to
blob_locations: the two have independent lifecycles, and an FK would
force location-before-params write ordering. Because nothing cascades,
DeleteLocation no longer shreds — a caller removing a blob must delete
from both tables.

Two fixes to the material itself, both raised in review:

- Add header_len. The stored blob is envelope||ciphertext and nothing
  recorded where the envelope ends, so a read could not locate byte 0 of
  the ciphertext without decoding the header, leaving the "no
  header round-trip" goal out of reach.
- Store the whole COSE Enc_structure as aad instead of the protected
  header. The structure's context string differs between a COSE_Encrypt
  and a COSE_Encrypt0, which a bare row cannot record, so the protected
  header alone is not enough to rebuild the AAD. The header stays
  recoverable from the Enc_structure as element 1.

Every column is NOT NULL, so the all-or-nothing invariant is structural
rather than a nullable-column check: the existence of a row is what marks
a blob as encrypted. BlobEncryptionParams.Validate rejects an incomplete
set before SQL, replacing ValidateFEE/ErrPartialFEE.

Migration 00013 is reshaped in place — it has not shipped anywhere.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
@bajtos

bajtos commented Aug 17, 2026

Copy link
Copy Markdown
Member

I updated the pull request with te following changes:

  • Moved the FEE wrap material out of blob_locations into a new ingot.blob_encryption_params table. Every column is NOT NULL with a CHECK, so the all-or-nothing invariant is structural: the existence of a row is what marks a blob as encrypted.
  • No foreign key to blob_locations, and DeleteLocation deliberately does not shred; crypto-shred is an explicit DeleteEncryptionParams.
  • header_len added and protected_header renamed to aad, storing the whole COSE Enc_structure.
  • BlobLocation is back to main's five fields exactly
  • New BlobEncryptionParams struct with a Validate() that names the missing fields,
  • A new EncryptionParamsStore interface sibling to LocationStore/InclusionStore
  • The now-unused nullInt64/nullBytes helpers are gone.

A side benefit: the segment re-ship path at server.go:333 re-upserts a location with no FEE fields, which under the old single-table full-row upsert would have silently nulled a stored CEK. That can no longer happen.

@bajtos
bajtos requested a review from alanshaw August 17, 2026 14:07
@bajtos
bajtos requested review from bajtos and pyropy and a lite review from Copilot August 17, 2026 14:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

A region-key rotation replaces the wrapped CEK and its key version and
nothing else, but the only way to write that was PutEncryptionParams,
which rewrites every column. The ciphertext is unchanged across a
rotation, so a caller that got the nonce, chunk size, AAD or header
length wrong on the re-supplied set would corrupt a decryptable row.

RewrapEncryptionParams takes only the two parameters that change:

- Postgres issues a plain UPDATE of the two columns. Zero rows affected
  means the blob has no row — not encrypted, or already shredded — so it
  returns ErrNotFound instead of reporting success.
- ValidateRewrap rejects an empty CEK or key version, so a rotation
  cannot blank out the material the decrypt path needs.

PutEncryptionParams keeps its upsert semantics for re-encryption, which
does replace the whole set.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@bajtos bajtos changed the title FEE wrap material on blob_locations (FIL-480) FEE wrap material on blob_encryption_params (FIL-480) Aug 17, 2026
Comment thread registry/stores.go Outdated
bajtos added 2 commits August 18, 2026 19:32
We will store keys in OpenBao.

Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Both sides added a table to the live store test's TRUNCATE list; the
resolution keeps both.

Both sides also added a migration numbered 00013, which goose rejects as
a duplicate version. Main's 00013_revocation_cursor is already published,
so the branch's blob_encryption migration moves to 00014.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
@bajtos

bajtos commented Aug 24, 2026

Copy link
Copy Markdown
Member

@codex review

@alanshaw alanshaw left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty much good to go. I have left some minor comments!

Comment thread inmem/stores.go Outdated
// cloneLocation deep-copies a BlobLocation's digest so the stored copy and any
// returned copy never alias the caller's slice.
func cloneLocation(loc registry.BlobLocation) registry.BlobLocation {
loc.Digest = cloneBytes(loc.Digest)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just use bytes.Clone?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Written by Claude.

Done, and removed the local cloneBytes helper in favour of bytes.Clone everywhere in the file.

Comment thread .claude Outdated
@@ -0,0 +1 @@
/Users/bajtos/src/fil-forge/ingot/.claude No newline at end of file

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Written by Claude.

Removed. It is a symlink my worktree setup creates, committed by accident. I left it out of .gitignore on purpose so the repo can still check in a shared .claude/ config later.

@@ -0,0 +1,49 @@
-- +goose Up
-- FEE (FilOne encryption envelope) per-blob encryption parameters. When Ingot

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically Filecoin Encryption Envelope not FilOne.

Suggested change
-- FEE (FilOne encryption envelope) per-blob encryption parameters. When Ingot
-- FEE (Filecoin Encryption Envelope) per-blob encryption parameters. When Ingot

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Written by Claude.

Fixed, here and in the Go doc comment.

CREATE TABLE ingot.blob_encryption_params (
space text NOT NULL,
digest bytea NOT NULL, -- ciphertext blob multihash, as in blob_locations
tenant_recipient_kid text NOT NULL -- opaque id of the Hilt wrap key (insurance-recovery unwrap)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to store this? It's not something we will ever use and is stored in the header anyway, right? Or am I misunderstanding?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this bytes or a DID?

EDIT: seems to be a DID with a fragment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Written by Claude.

You are right, dropped. The table caches only what a range GET needs to skip the header fetch, and that read unwraps via the region KEK. The insurance-recovery unwrap is rare and out-of-band, and it reads the envelope header anyway, where the COSE recipients live. Column, struct field, and tests are gone.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Written by Claude.

Moot now, the column is gone. It was a DID with a fragment.

Comment thread registry/stores.go Outdated
}

// BlobEncryptionParams is one row of ingot.blob_encryption_params: the FEE
// (FilOne encryption envelope) parameters a read needs to decrypt an encrypted

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// (FilOne encryption envelope) parameters a read needs to decrypt an encrypted
// (Filecoin Encryption Envelope) parameters a read needs to decrypt an encrypted

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Written by Claude.

Applied.

Comment thread registry/stores.go Outdated

// TenantRecipientKID identifies the Hilt wrap key used for insurance-recovery
// unwrap. Opaque, so it is agnostic to the tenant-vs-bucket granularity
// decision (FIL-574).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets not add references to private linear tickets in OSS code.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Written by Claude.

Dropped both ticket ids from the doc comments. The explanation stands without them.

Comment thread registry/stores.go Outdated
// an AAD with no nonce, or an empty (non-nil) byte slice standing in for a real
// value — would persist a row a later GET cannot decrypt, so
// PutEncryptionParams rejects it before touching the store.
func (p BlobEncryptionParams) Validate() error {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically I'd follow the parse, don't validate approach. When the data has been parsed/decoded from untrusted source at an API boundary or is created by a constructor there's not a need to re-validate again. It's just duplication, and in this case duplication that is also applied by the database layer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Written by Claude.

Agreed. Validate and ErrInvalidEncryptionParams are gone; the NOT NULL/CHECK constraints are the single enforcement point. The live test now asserts the constraint rejects a partial set instead of asserting a Go error. The inmem fake will accept a row Postgres would refuse, which seems right for a fake.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf5de99614

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .claude Outdated
Comment thread registry/stores.go Outdated
Address review on #15.

Drop tenant_recipient_kid. The table caches only what a range GET needs
to skip the header fetch, and that read unwraps via the region KEK. The
insurance-recovery unwrap is rare, out-of-band, and reads the envelope
header anyway, where the COSE recipients already live.

Drop BlobEncryptionParams.Validate. Every column is NOT NULL with a
CHECK, so a half-populated set was being rejected twice.

Also: FEE expands to Filecoin Encryption Envelope, not FilOne; use
bytes.Clone in place of the local cloneBytes helper; untrack the
worktree's .claude symlink; and drop the Linear ticket ids from the
doc comments.

Assisted-by: Claude:claude-opus-5

Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
@bajtos

bajtos commented Aug 24, 2026

Copy link
Copy Markdown
Member

Written by Claude.

Since the update comment above: tenant_recipient_kid and BlobEncryptionParams.Validate are gone per review. Migration 00014 is reshaped in place, so a persistent dev DB that already applied the old version needs a reset — the leftover table keeps tenant_recipient_kid NOT NULL and the new insert omits that column.

The INGOT_TEST_DSN-gated live tests still have not run locally (no DB in this sandbox); CI applies the migrations fresh and covers the new shape.

@bajtos
bajtos requested a review from alanshaw August 24, 2026 10:04
@alanshaw
alanshaw merged commit fc5be24 into main Aug 24, 2026
10 checks passed
@alanshaw
alanshaw deleted the claude/fil-480-cipgkz branch August 24, 2026 10:16
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.

6 participants