Skip to content

build: freenet-stdlib 0.6→0.8 upgrade + delegate/room-contract migration - #394

Merged
sanity merged 5 commits into
mainfrom
dogfood/stdlib-0.8-migrate
Jul 12, 2026
Merged

build: freenet-stdlib 0.6→0.8 upgrade + delegate/room-contract migration#394
sanity merged 5 commits into
mainfrom
dogfood/stdlib-0.8-migrate

Conversation

@sanity

@sanity sanity commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

⚠️ WIP DOGFOOD — DO NOT MERGE

This branch dogfoods the freenet-stdlib 0.6.0 → 0.8.2 upgrade to prove that room + delegate data survives before we ever touch the shared production River contract (raAqMhMG7KUpXBU2SxgCQ3Vh4PYjttxdSWd9ftV7RLv). It is not for merge as-is: it pins stdlib via a [patch.crates-io] to a local checkout (so CI will fail to resolve deps — expected), and the full browser delegate round-trip was not executed live in this pass.

What changed

  1. stdlib 0.6.0 → 0.8.2 (workspace Cargo.toml), patched to the local freenet-stdlib checkout until the leak-fix 0.8.3 is published to crates.io. The leak fix is WebApi(browser-client)-only and does not affect the contract/delegate WASM bytes this upgrade re-keys, so the patched 0.8.2 is byte-equivalent to what we'll ship on for the migration surface.

  2. Fixed the one real 0.8 API break (DEFAULT_CIPHER/DEFAULT_NONCE) in ui/src/components/app/chat_delegate.rs. stdlib 0.8 removed these constants — they were a world-known XChaCha20 key + nonce. Replaced with a random 32-byte cipher + 24-byte nonce generated once per install and persisted in localStorage.

    • Key finding: a node built against stdlib-0.8 core (freenet-core #4140/#4143) ignores the client-supplied cipher/nonce entirely — register_delegate derives a per-delegate DEK from the node's own KEK via HKDF, and decrypts pre-0.8 secrets (written under the old world-known cipher) via a built-in LEGACY_DEFAULT_CIPHER fallback (decrypt_secret_blob Tier-3). So old-secret decrypt/re-encrypt is the node's job, not River's. Persisting a per-install random value is the correct, forward-compatible choice: harmless on 0.8 nodes (discarded), and a per-install secret (not world-known) on any older node that still honors it.
  3. Registered the last stdlib-0.6 generation so the 0.8 build migrates from it:

    • legacy_delegates.toml V25 = 8bb5d06f… (delegate_key 3e0e0737…)
    • common/legacy_room_contracts.toml V25 = 58a5e73c…
  4. Rebuilt room-contract + chat-delegate WASM against 0.8 (re-keys):

    Artifact OLD 0.6 code_hash NEW 0.8 code_hash
    room_contract.wasm 58a5e73c…e47c50 18227bb6…a7b151
    chat_delegate.wasm 8bb5d06f…51cc8d f4cea6bf…cf24b48
    delegate_key 3e0e0737…10eda80 da2ce32f…bc65eb61

Migration wiring decision

River's bespoke probe machinery (fire_legacy_migration_request + the #292 room backward-probe in common/src/migration.rs) is kept as-is. freenet-migrate 0.1.0 is pure/synchronous and has no WebSocket-driver equivalent for River's app-side probe loop, so a full crate swap is out of scope this pass. The requirement met here is correctness: the 0.6 legacy hashes are registered so the existing (24-prior-bumps-proven) probe machinery carries data forward. freenet-migrate's clean-fit surface (build-time codegen + hash-guard + key derivation) is byte-identical to River's derivation (proven by the crate's own codegen_stdlib_consistency test + River delegating to stdlib's ContractKey::from_params), and is a good candidate for a later, separate adoption PR.

Verified

  • ✅ River builds against 0.8: room-contract + chat-delegate WASM (--locked, reproducible), river-ui compiles (cargo check, wasm target).
  • ✅ Migration registry tests pass (migration_test + room_contract_migration_test, 4+4), validating the V25 entries and delegate_key == BLAKE3(code_hash).
  • Delegate/contract ABI is 0.6↔0.8 compatible (the make-or-break): the OLD 0.6 delegate WASM imports the exact same host functions with byte-identical type signatures as 0.8 (get_secret/set_secret/remove_secret/get_secret_len/logger.info); the room-contract imports match too. So the installed 0.8.2 node can execute the 0.6 WASMs — the migration's core requirement holds.
  • freenet-migrate 0.1.0 test suite green against local stdlib 0.8.2.

NOT verified live (yet)

  • The full browser delegate-migration round-trip (create room+message+DM+signing-key under the 0.6 UI, then load the 0.8 UI and confirm the room list / per-room signing keys / DM history migrate forward). Delegate migration is browser-only; this is the one remaining live gap.
  • The pre-0.8-node → node-upgraded-to-0.8 cross-version secret path (old secrets written under the real world-known cipher, then node upgraded). A fresh local 0.8 node writes secrets under derived DEKs, so it does not reproduce this.

Live riverctl-driven room-contract migration test against a real 0.8 node is running separately; result to follow in a comment.

🤖 Generated with Claude Code

[AI-assisted - Claude]

…ration (WIP, do not merge)

WIP dogfood of the stdlib 0.6.0 -> 0.8.2 upgrade to prove room + delegate
data survives before ever touching the shared River contract. NOT FOR MERGE
as-is (uses a [patch.crates-io] to a local stdlib checkout pending the 0.8.3
release, and has NOT been published/verified end-to-end in the browser).

What this does:
- Bump freenet-stdlib 0.6.0 -> 0.8.2 (workspace), patched to the local
  checkout until the leak-fix 0.8.3 is on crates.io (leak fix is WebApi-only
  and does not affect contract/delegate WASM bytes).
- Fix the one real 0.8 API break: DelegateRequest::DEFAULT_CIPHER/DEFAULT_NONCE
  were removed (they were a world-known XChaCha20 key/nonce). Replace with a
  random 32-byte cipher + 24-byte nonce generated once per install and
  persisted in localStorage. NOTE: a stdlib-0.8 node IGNORES the client cipher
  (it derives a per-delegate DEK from the node KEK via HKDF, freenet-core
  #4140/#4143) and decrypts pre-0.8 secrets via its LEGACY_DEFAULT_CIPHER
  fallback, so old-secret decryption is the node's job, not River's.
- Register the last stdlib-0.6 generation so the 0.8 build migrates FROM it:
  * legacy_delegates.toml V25 = 8bb5d06f... (delegate_key 3e0e0737...)
  * common/legacy_room_contracts.toml V25 = 58a5e73c...
- Rebuild room-contract + chat-delegate WASM against 0.8 (new keys):
  * room_contract.wasm 58a5e73c -> 18227bb6
  * chat_delegate.wasm 8bb5d06f -> f4cea6bf (delegate_key 3e0e0737 -> da2ce32f)

Migration wiring: River's bespoke probe machinery (fire_legacy_migration_request
+ #292 room backward-probe) is kept as-is; freenet-migrate 0.1.0 has no
WS-driver equivalent for it, so a full crate swap is out of scope this pass.
The requirement met here is correctness: the 0.6 legacy hashes are registered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CD8VBzb8qsDvdcPdF6TNQG
@sanity

sanity commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Live verification: room-contract migration 0.6→0.8 WORKS on a real 0.8.2 node

Ran a riverctl-driven room-contract migration against an isolated freenet local 0.8.2 node (OLD riverctl built from origin/main @ 74862fd8, stdlib 0.6.1; NEW riverctl from this branch, patched stdlib 0.8.2). Both WASM b3sums matched the registered hashes (OLD 58a5e73c…, NEW 18227bb6…).

Keys derived (base58):

  • Owner VK / room id: HtxpCJFU3ZSU7PYDHQLG5DoCjApnfEm6YwN8qyjaUQnE
  • OLD 0.6 contract key: FhoNSwKH3PTJqC9g7FH1PzZGK9tPjeBJjt71jgVYQ2n1
  • NEW 0.8 contract key: 6kzKtterGwcf3KND4sS22uADynrKiyBWwctBBoqRiMbb (distinct — confirms re-key)

What was proven:

  1. The 0.8 node runs the 0.6 contract WASMroom create (PUT), message send, message list all succeeded under the OLD key (message list returned hello-from-old-0.6-contract). Confirms the ABI compat established statically.
  2. Identity/room import can't recover a room several contract-WASM generations behind (restores stale "old IDs") #292 backward-probe recovery + forward-migration works. A fresh client (empty config, knows only the owner key) logged:
    Room not present on current contract 6kzKtterGwcf…; probing 25 previous contract generation(s)
    Recovered room from a previous contract generation (probe 1/25)
    Migrated recovered room forward onto current contract 6kzKtterGwcf…
    STDOUT: [alice]: hello-from-old-0.6-contract
    
    Probe 1/25 = registry entry V25 = the OLD 0.6 hash added in this PR.
  3. Migration persisted — a second fresh client read the message directly on the NEW key with no probe; independent fdev (0.8.2) execute get showed byte-identical state (1190 bytes, b3sum e318306b…) under both old and new keys, message bytes visible in the CBOR.

Bug found (pre-existing on main, NOT a regression): the in-place upgrade path (ensure_room_migrated, cli/src/api.rs:2039) aborts fatally with missing contract on an isolated node before recovery runs, because the "contract absent → migrate" logic is only in the timeout branch, not the prompt-error arm. Filed as #395. Its production severity depends on networked timeout-vs-prompt-error behavior (untested). Once the new contract exists on the node (via any fresh-client recovery or room republish), the in-place path works.

Still NOT verified live: the browser delegate migration round-trip (room list / per-room signing keys / DM history) — delegate migration is browser-only; riverctl doesn't touch the chat delegate. The delegate side is verified at the static level (ABI import/signature parity 0.6↔0.8, V25 compiled into LEGACY_DELEGATES, unchanged probe mechanism from 24 prior bumps).

[AI-assisted - Claude]

sanity and others added 4 commits July 11, 2026 09:31
Replace the `[patch.crates-io] freenet-stdlib = { path = ... }` dogfood
override with a clean `freenet-stdlib = "0.8.3"` (now on crates.io) across
the workspace, and update Cargo.lock to resolve 0.8.3 from the registry
(checksum 2ce69f7b...). No other transitive deps change.

Verified: published freenet-stdlib 0.8.2 and 0.8.3 produce BYTE-IDENTICAL
contract/delegate WASM (room 74f3dff1, delegate d91e33fc). The 0.8.3 diff
is confined to the browser WebApi client (net feature) plus a dead-code
clippy cleanup in contract_interface/encoding.rs; the leak fix does NOT
touch contract-reachable code. river-ui (wasm32), riverctl, and river-core
all compile clean against 0.8.3.

BLOCKER (not resolved here, pre-existing on this branch): the committed
dogfood WASM (room 18227bb6 / delegate f4cea6bf / delegate_key da2ce32f)
is NOT reproducible — a canonical `cargo make sync-wasm` (toolchain 1.94.1,
--locked, against 0.8.2 OR 0.8.3) produces room 74f3dff1 / delegate
d91e33fc / delegate_key b61dc292 instead. Before republish the WASM must be
regenerated via sync-wasm and the room+delegate migration round-trips
re-verified against the real shippable keys. Committed WASM left untouched
(restored) pending that decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CD8VBzb8qsDvdcPdF6TNQG
… stale artifact)

The previously-committed room_contract/chat_delegate WASM on this branch
(room 18227bb6…, delegate f4cea6bf…, delegate_key da2ce32f…) was a
non-reproducible, dogfood-local artifact: a canonical build does not
reproduce it, so CI check-cli-wasm / the reproducibility gate would go red.

Regenerated via the canonical co-build path (cargo make sync-wasm) with
freenet-stdlib = 0.8.3 (crates.io, no patch), toolchain 1.94.1, --locked.
Verified reproducible: two independent from-scratch builds are byte-identical.

  room  code_hash (b3sum) : 74f3dff1c3c2f4ef89e4c93ee45ddb62952df22161450a905c2784c1facf6740
  delegate code_hash      : d91e33fcf47b4863b996fbf6bb8d782b2250a0e5037fb8dc7a030b60ac1c4de1
  delegate_key            : b61dc292c952f5ec48f870a0f9829090968a7d03669671f619487f0b5aca8633

The stale artifact was never published (no user data on it), so it needs
no legacy entry. The migration anchor remains V25 (the real last stdlib-0.6
generation): main's committed delegate b3sum 8bb5d06f… and room b3sum
58a5e73c… both match the V25 entries, so check-migration passes against base.

ui/public/contracts/ and cli/contracts/ room WASM kept in sync (check-cli-wasm).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CD8VBzb8qsDvdcPdF6TNQG
In production River runs inside the gateway iframe with an opaque origin,
so `window.localStorage` throws SecurityError. Both
`load_persisted_cipher_material` and `persist_cipher_material` then
silently no-op, so the pre-fix `chat_delegate_cipher_material()`
generated a FRESH random cipher/nonce on every `set_up_chat_delegate()`
call — which fires on every reconnect and sleep/wake — rotating the
client-supplied delegate cipher within a single page.

Add a process-memory fallback (`IN_MEMORY_CIPHER_MATERIAL`, a
`thread_local!` — wasm is single-threaded) used when localStorage is
unavailable: the cipher/nonce is generated once and stays stable for the
page lifetime. Order is unchanged where storage works — try localStorage
first, write-through when available — so a normal top-level origin still
gets cross-reload stability; only the storage-denied iframe path changes,
from per-call rotation to page-stable.

Also:
- Correct the doc-comment to describe the real persistence model
  (localStorage where available, page-stable in-memory otherwise) and to
  note that 0.8 nodes ignore the client cipher (KEK-derived DEK), so
  cross-reload rotation is harmless on the 0.8 fleet; the only residual
  is a pre-0.8 cipher-honoring node.
- Fix the legacy migration TOML descriptions: "0.6.0 -> 0.8.2" -> "0.8.3"
  to match the actual stdlib pin.
- Add `cipher_material_is_stable_within_process` test pinning the fix.

UI-only change: the cipher code compiles into river-ui, NOT the delegate
WASM. `cargo make sync-wasm` reproduces byte-identical artifacts —
chat_delegate d91e33fc…, room_contract 74f3dff1…, delegate_key
b61dc292… — so migration verification is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CD8VBzb8qsDvdcPdF6TNQG
Two follow-ups to the chat-delegate cipher-stability fix:

1. Correct the safety doc-comment. The earlier wording hedged that the
   pre-0.8 cipher-honoring brick "may be unreachable if the 0.6→0.8
   client-API wire is incompatible". Verified (#394) that it
   is NOT: there is no protocol-version handshake and the
   `RegisterDelegate` bincode layout is byte-identical from stdlib
   v0.1.22 through 0.8.3, so a stdlib-0.8 River client CAN connect to and
   register a delegate on a pre-0.8 node. The residual is therefore
   reachable at the protocol level; what makes it unlikely is that a
   cipher-honoring node is freenet-core ≲0.2.58 (pre-#4143/#4144), which
   the live ~0.2.9x auto-updated fleet almost certainly no longer runs.
   The doc now also states the fix is partial for that node class: it
   removes within-page (reconnect/sleep-wake) rotation but not
   cross-reload rotation in the storage-denied iframe.

2. Bump riverctl 0.1.73 -> 0.1.74 (+ Cargo.lock). The stdlib 0.6→0.8 bump
   changed room_contract.wasm, and check-wasm-sync fails when the CLI
   version still matches the crates.io-published one (riverctl embeds the
   WASM to derive the contract key). This is the check's own prescribed
   fix; no WASM changes (delegate/room-contract hashes unchanged:
   chat_delegate d91e33fc…, room_contract 74f3dff1…).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CD8VBzb8qsDvdcPdF6TNQG
@sanity

sanity commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

Blocker 1 resolved: chat-delegate cipher stability in the sandboxed gateway iframe

The bug

In production River runs inside the gateway iframe with an opaque origin, so window.localStorage throws SecurityError. Both load_persisted_cipher_material() and persist_cipher_material() (ui/src/components/app/chat_delegate.rs) then silently no-op, so chat_delegate_cipher_material() generated a fresh random cipher/nonce on every set_up_chat_delegate() — which fires on every reconnect and every sleep/wake — rotating the client-supplied delegate cipher within a single page.

The fix

Added a process-memory fallback (IN_MEMORY_CIPHER_MATERIAL, a thread_local! — wasm is single-threaded, so effectively process-global): when localStorage is unavailable the cipher/nonce is generated once and stays stable for the page lifetime. Order is unchanged where storage works (try localStorage first, write-through when available), so a normal top-level origin keeps cross-reload stability; only the storage-denied iframe path changes, from per-call rotation to page-stable. Pinned by cipher_material_is_stable_within_process.

Pre-0.8 reachability assessment (the safety justification)

On stdlib-0.8 nodes the node ignores the client-supplied cipher/nonce and derives a per-delegate DEK from its own KEK (freenet-core docs/secrets-at-rest.md; wasm_runtime/secrets_store/store.rs), so cipher rotation is harmless across the whole 0.8 fleet. The only place the client cipher still matters is a pre-0.8 node that honors it.

I checked whether a stdlib-0.8 River client can even reach such a node. It can — the residual is not gated out by wire incompatibility:

  • No protocol-version handshake. The node selects only an encoding (EncodingProtocol Native/bincode vs Flatbuffers; River pins ?encodingProtocol=native) in freenet-core .../client_events/websocket.rs; it never rejects a client by stdlib version.
  • RegisterDelegate bincode layout is byte-identical from stdlib v0.1.220.8.3: ClientRequest/DelegateRequest/HostResponse variant ordering unchanged, and RegisterDelegate { delegate, cipher, nonce } fields/order unchanged. The flatbuffer schema for it is also unchanged. The only 0.6→0.8 bincode break (NodeDiagnosticsResponse.contract_states map-key type, stdlib 0.7.0) is an unrelated HostResponse::QueryResponse variant never exercised by the delegate-register exchange.

So the pre-0.8 cipher-honoring case is reachable at the protocol level. What makes it unlikely in practice: a cipher-honoring node is freenet-core ≲ 0.2.58 (stdlib pre-0.8, ~April 2026, before #4143/#4144); every release from ~0.2.60 ignores the client cipher, and the live network runs ~0.2.9x with auto-update. Such an ancient non-updated peer almost certainly no longer exists, but I can't prove from code that none do.

Honest scope of the fix: it removes the within-page (reconnect / sleep-wake) rotation, which was the actual regression. In the iframe it does not make the material stable across a full page reload (localStorage still no-ops there), so for a hypothetical pre-0.8 honoring node it reduces rather than fully eliminates the theoretical cross-reload brick. On the entire 0.8 fleet this is moot (client cipher ignored).

WASM unchanged (migration verification still holds)

The cipher code compiles into river-ui, not the delegate WASM. cargo make sync-wasm reproduces byte-identical artifacts:

  • chat_delegate.wasm = d91e33fc… (delegate_key b61dc292… = BLAKE3(BLAKE3(wasm)))
  • room_contract.wasm = 74f3dff1… (ui/ and cli/ in sync)

check-migration / check-room-contract-migration confirm no migration entry is needed.

Also in this change

  • Corrected the two legacy-migration TOML descriptions: 0.6.0 -> 0.8.20.8.3 (matching the actual pin).
  • Bumped riverctl 0.1.730.1.74 so check-wasm-sync passes (the 0.6→0.8 bump changed room_contract.wasm, and that check requires the CLI version to differ from the crates.io-published one).

[AI-assisted - Claude]

@sanity

sanity commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Full-tier review complete — GO for republish

Reproducible canonical build. room 74f3dff1 / delegate d91e33fc / delegate_key b61dc292, built twice byte-identical.

Both migrations re-verified against the real shippable keys on an isolated 0.8 node:

  • Room migration (backward-probe → new key): recovered message byte-identical.
  • Delegate migration: room list, per-room signing keys, private-room secret, old messages, and rooms_meta all carried across; 10→10 secrets; zero #204/deserialize errors; data confirmed on the real b61dc292 namespace.

External Codex + adversarial cipher-security review: clean. The one finding (cipher rotation in the sandboxed gateway iframe) is fixed via a thread_local page-stable in-memory fallback plus an honest doc note. Documented residual: a pre-~0.2.58 cipher-honoring node, effectively unreachable on the 0.2.9x fleet.

Legacy-registry audit complete. V25 anchors the last live 0.6 generation; no missing live generation.

CI green on ed54ec7d (build, check-wasm-sync, check-delegate-migration, check-room-contract-migration, ui-playwright-tests all SUCCESS).

Scope notes. Orthogonal pre-existing bug #395 (riverctl in-place upgrade) does not affect fresh-client recovery. Republish is a deliberate, in-the-loop operation: it re-keys the shared contract, old invite URLs die, and the Official room is recreated.

[AI-assisted - Claude]

@sanity sanity changed the title build: [WIP dogfood, do not merge] freenet-stdlib 0.6→0.8 upgrade + migration build: freenet-stdlib 0.6→0.8 upgrade + delegate/room-contract migration Jul 12, 2026
@sanity
sanity marked this pull request as ready for review July 12, 2026 12:34
@sanity
sanity merged commit 47be34e into main Jul 12, 2026
7 checks passed
sanity added a commit to freenet/delta that referenced this pull request Jul 12, 2026
…fix + contract/delegate re-key) (#32)

* chore: register legacy 0.6 contract/delegate hashes before stdlib 0.8.3 bump

Records the current freenet-stdlib 0.6.0 delegate (V10) and contract (C5)
WASM code hashes in the legacy registries BEFORE the 0.8.3 rebuild re-keys
both WASMs. Without this, the UI's backward-probe migration could not find
existing sites' state / delegate secrets under the old keys after upgrade,
silently orphaning every user's data.

  delegate V10 code_hash: 4d576358229eed2ea567d8c3275776f584a0a66f41b4b05058644da4d4fc56aa
  contract C5 code_hash:  8e36f95d436899960cf31afd76586837a3bf9f91483dcd09d26483ad02823c53

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

* feat: bump freenet-stdlib 0.6.0 -> 0.8.3 (inbound-WS FileReader leak fix)

Delivers Delta the inbound-WebSocket FileReader memory-leak fix that
River already shipped: Delta's UI uses stdlib's `net` feature, which had
the same leak (every inbound WS message leaked a FileReader).

Compile break fixed: stdlib 0.8 removed the world-known
`DelegateRequest::DEFAULT_CIPHER` / `DEFAULT_NONCE` constants the UI passed
to `RegisterDelegate`. Replaced with a per-install random cipher/nonce
(persisted to localStorage where available, page-stable in-memory fallback
in the sandboxed gateway iframe), mirroring River's field-proven fix
(freenet/river#394). On a stdlib-0.8 node the value is ignored server-side
(the node derives the DEK from its own KEK, with a legacy fallback for
pre-0.8 world-known-cipher secrets), so this is confidentiality hardening
plus supplying the now-required fields — not a migration dependency.

The contract and delegate WASM compiled against 0.8.3 with NO source
changes (the delegate uses the high-level `ctx.set_secret`/`get_secret`
API, which abstracts the cipher).

WASM re-key (content-addressed identity changed on rebuild):
  delegate  0.6 4d576358.. -> 0.8 06b7e80f..  (registered as legacy V10)
  contract  0.6 8e36f95d.. -> 0.8 7f81b4d6..  (registered as legacy C5)
Both 0.6 predecessors were registered in the prior commit so the UI's
backward-probe migration carries existing sites' state and delegate
secrets forward. WASMs build byte-identically (verified via two forced
clean rebuilds; toolchain pinned at 1.94.1, same as River).

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

---------

Co-authored-by: Claude Opus 4.8 <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