Skip to content

feat: Shadow simulator compatibility behind shadow-integration feature#420

Merged
MegaRedHand merged 2 commits into
mainfrom
shadow-integration
Jun 8, 2026
Merged

feat: Shadow simulator compatibility behind shadow-integration feature#420
MegaRedHand merged 2 commits into
mainfrom
shadow-integration

Conversation

@MegaRedHand

@MegaRedHand MegaRedHand commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

What

Integrates Shadow network simulator compatibility (ref: kamilsa/ethlambda@ed5a447), gated so the default build, main, and the committed Cargo.lock are unchanged. Used for the lean-shadow-fuzzer integration. See shadow/README.md for full docs.

Three Shadow build changes

Change Reason How it's gated
Drop jemalloc allocator + malloc_conf export Correctness. jemalloc with unprefixed_malloc interposes the global C malloc and self-deadlocks at startup under Shadow (shadow#3763): the shim fopens /proc/self/maps on its first intercepted syscall, calling malloc, which re-enters jemalloc mid-init while it holds its non-recursive init lock jemalloc is a default feature pulling the optional tikv-jemallocator dep; Shadow build uses --no-default-features
quinn-udp -> fallback (send_to/recv_from) Correctness. Shadow's UDP emulation lacks GSO/GRO batch syscalls (sendmmsg, segmentation offload) [patch] injected at build time (see below)
#[tokio::main] -> current_thread flavor Optimization only. Shadow single-steps execution, so multi-threaded worker threads add only scheduling noise, never parallelism cfg(feature = "shadow-integration")

jemalloc: the jemalloc feature

jemalloc is on by default and pulls the optional tikv-jemallocator dependency. Cargo features are additive, so a feature can't subtract a default-on dependency, and merely disabling #[global_allocator] isn't enough (the unprefixed_malloc interposition is what deadlocks). So the Shadow build drops it with --no-default-features. A compile_error! catches the footgun of enabling shadow-integration while jemalloc is still linked.

quinn-udp: patch injected at build time (not committed)

A Cargo [patch.crates-io] table cannot be feature-gated (patches are global). To keep normal builds and the lockfile pristine, the patch is not in the committed Cargo.toml. Instead shadow/build.sh appends it (from shadow/cargo-patch.toml) to the manifest, runs the build, and restores Cargo.toml + Cargo.lock afterward. The dormant shadow/quinn-udp-patch crate lives in the repo but is referenced only during Shadow builds.

Building

# Normal build (jemalloc + multi-threaded runtime) -- unchanged from main
cargo build --release

# Shadow-compatible binary (injects patch, drops jemalloc, single-threaded)
make shadow-build

# Shadow-compatible Docker image
make shadow-docker-build

Verification

  • cargo check + clippy pass for the default build, --no-default-features, and the Shadow config.
  • Full make shadow-build and default cargo build --release --locked both compile; Cargo.toml/Cargo.lock restored cleanly after the Shadow build.
  • Committed Cargo.lock is byte-identical to main.
  • The mutual-exclusion compile_error! fires when shadow-integration is enabled without --no-default-features.
  • Verified under Shadow via lean-shadow-fuzzer: a 4-node ethlambda network (Linux/arm64 image built from this branch) boots with no jemalloc startup deadlock (Using system allocator), forms a full QUIC mesh, gossips blocks + attestations, and reaches finality (all nodes head_slot=14 justified_slot=12 finalized_slot=11) over a 120s simulated run, with no panics.

Notes

  • jemalloc_pprof is retained: without the allocator its PROF_CTL is None, so /debug/pprof/allocs degrades gracefully rather than failing to compile.
  • Dockerfile gains SHADOW, NO_DEFAULT_FEATURES, and LOCKED build args (Shadow builds inject the patch and build unlocked, since the patch is absent from the committed lockfile).

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

🤖 Kimi Code Review

Review of PR #420 (Shadow simulator integration)

The PR introduces Shadow simulator compatibility by patching quinn-udp to use fallback syscalls and adding feature-gated build configuration. The approach is sound, but several minor issues need attention.

Critical Issues

1. Typo in error message
patches/quinn-udp/src/lib.rs:88

last_send_error.lock().expect("poisend lock");

Change "poisend" to "poisoned".

Code Quality & Safety

2. Unused imports when logging disabled
patches/quinn-udp/src/lib.rs:9-12
Mutex and Instant are only used when tracing or direct-log features are enabled. Gate them:

#[cfg(any(feature = "tracing", feature = "direct-log"))]
use std::sync::Mutex;
#[cfg(any(feature = "tracing", feature = "direct-log"))]
use std::time::Instant;

3. Unsafe block needs justification
patches/quinn-udp/src/fallback.rs:47-49
The transmute from &mut [IoSliceMut] to &mut [MaybeUninitSlice] is sound because MaybeUninitSlice is #[repr(transparent)] over IoSliceMut, but this invariant should be documented:

// Safety: MaybeUninitSlice is #[repr(transparent)] over IoSliceMut,
// guaranteeing identical layout.
let bufs = unsafe { ... };

4. Error swallowing behavior
patches/quinn-udp/src/fallback.rs:24-29
Non-WouldBlock send errors are logged then discarded (Ok(())). This diverges from standard Quinn behavior where errors propagate. Document that this is intentional for Shadow compatibility, or consider returning the error after logging.

Build System

5. Dockerfile ARG expansion
Dockerfile:42 and Dockerfile:47
Using $NO_DEFAULT_FEATURES unquoted is safe here (values are --no-default-features or empty), but be aware that empty arguments to cargo can be fragile. The current implementation is acceptable.

Consensus/Security Note

This PR modifies networking infrastructure only; no consensus-critical code (state transition, fork choice, XMSS verification) is affected. The quinn-udp fallback disables GSO/GRO batching (reducing throughput), which is appropriate for Shadow's emulation model.

Summary

The PR is architecturally correct. Fix the typo (Item 1), address the unused imports (Item 2), and document the unsafe block (Item 3) before merging. The error handling (Item 4) should be verified as intentional for Shadow's requirements.


Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

🤖 Codex Code Review

  1. High: Transmit::segment_size is ignored in the fallback sender. quinn-udp uses segment_size to represent multiple UDP datagrams packed into one Transmit. This implementation always calls send_to(transmit.contents, ...), so any batched QUIC transmit becomes one oversized datagram instead of N datagrams. On MTU-sized traffic that can turn into EMSGSIZE; worse, send() then logs and returns Ok(()), which silently drops the packet. The fallback needs to split contents by segment_size and send each chunk separately.

  2. Medium: the workspace-level patch makes the stripped-down UDP backend active for every build, not only shadow-integration (Cargo.toml, bin/ethlambda/Cargo.toml). That backend hard-disables ECN and destination IP reporting (quinn-udp/src/lib.rs, fallback.rs) and forces BATCH_SIZE/GSO/GRO down to 1 (fallback.rs, fallback.rs). That is a global QUIC transport regression, not a Shadow-only change. Even if acceptable for simulation, it should not be the default path for normal nodes.

  3. Medium: the vendored quinn-udp now only defines UdpSockRef conversion on Unix via AsFd (quinn-udp/src/lib.rs, quinn-udp/src/lib.rs). Since the patch is unconditional, this appears to break non-Unix targets that previously relied on upstream quinn-udp platform support. If Windows is intentionally unsupported, that needs to be explicit; otherwise this patch narrows platform compatibility.

No fork-choice, attestation, STF, XMSS, or SSZ logic changed in this PR, so I don’t have consensus-layer findings beyond the transport-level availability risks above.

I couldn’t verify with cargo check locally because rustup attempted to write under a read-only /home/runner/.rustup/tmp, so feature-set compileability remains unverified in this environment.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

Here is my review:


PR Review: feat: Shadow simulator compatibility behind shadow-integration feature

Overview: This PR gates Shadow network simulator compatibility behind a shadow-integration feature flag (single-threaded tokio runtime, no jemalloc), while applying an unconditional [patch.crates-io] for a stripped-down quinn-udp that replaces GSO/GRO batch syscalls with plain send_to/recv_from. The build ergonomics (make shadow-build, Docker arg) and mutual-exclusion compile_error! are well-designed.


Significant Issue: [patch.crates-io] affects all builds on main

The commit message explicitly states "it never lands on main", yet the PR targets main. If merged, every cargo build on main — including normal production builds — will use the fallback quinn-udp (plain send_to/recv_from, no GSO/GRO). This is a silent performance regression for all non-Shadow builds.

The PR description acknowledges this directly: "A plain cargo build on this branch is identical to main except for the quinn-udp UDP fallback." That exception is significant: GSO allows sending multiple UDP datagrams in a single syscall, which meaningfully affects throughput under load.

Options:

  • Keep this as a long-lived shadow branch (never merge to main), matching the zeam model described in the PR.
  • Or, if landing on main is intentional, explicitly acknowledge the performance trade-off in the PR description and remove the "never lands on main" claim from the commit message.

Unsafe transmute needs a SAFETY comment

patches/quinn-udp/src/fallback.rs:43-46:

let bufs = unsafe {
    &mut *(bufs as *mut [IoSliceMut<'_>] as *mut [socket2::MaybeUninitSlice<'_>])
};

This relies on IoSliceMut and MaybeUninitSlice having identical memory layout (both wrap iovec on Unix). This invariant holds today, but the compiler provides no guarantee. A // SAFETY: comment is required here, e.g.:

// SAFETY: IoSliceMut<'_> and MaybeUninitSlice<'_> are both transparent
// wrappers over `libc::iovec`, so they are guaranteed to have the same
// memory layout. This cast is safe on all Unix targets.

Unchecked meta[0] access could panic

patches/quinn-udp/src/fallback.rs:50:

meta[0] = RecvMeta { ... };
Ok(1)

If meta is empty, this panics. While quinn is expected to always pass a non-empty slice (and BATCH_SIZE = 1 makes this likely), an assertion would make the invariant explicit:

debug_assert!(!meta.is_empty());
meta[0] = RecvMeta { ... };

cfg_aliases build-dependency has no corresponding build.rs

patches/quinn-udp/Cargo.toml lists cfg_aliases as a build-dependency, but no build.rs file appears in the diff. Either:

  • The dependency is vestigial (copied from the upstream Cargo.toml) and should be removed, or
  • A build.rs is missing.

Since the patch unconditionally uses fallback.rs and has no platform-specific dispatching, cfg_aliases serves no purpose here and should be removed.


Typo

patches/quinn-udp/src/lib.rs:88:

last_send_error.lock().expect("poisend lock")
//                              ^^^^^^^ should be "poisoned lock"

Minor observations

  • patches/quinn-udp/src/fallback.rs:90-92 — The send helper can be simplified: send_to already returns io::Result<usize>, so socket.0.send_to(...)?; Ok(()) can be written socket.0.send_to(...).map(|_| ()).

  • patches/quinn-udp/src/lib.rsUdpSockRef::From is #[cfg(unix)]-only; on Windows this crate won't compile. Since Shadow is Linux-only this is acceptable, but a comment noting the Windows limitation would avoid confusion.

  • Dockerfile — The $NO_DEFAULT_FEATURES variable is correctly left unquoted so it word-splits to --no-default-features when set; quoting it would pass it as a single string to cargo and break the flag. This is intentional but non-obvious; a comment would help future readers.

  • The compile_error! guard and the jemalloc_pprof graceful degradation are correct and well-implemented. The logging message consolidation ("Using system allocator" covering both MSVC and non-jemalloc) is a clean simplification.


Summary

The feature-gating design is sound. The main decision point is whether the quinn-udp patch belongs on main at all — the PR contradicts itself on this. Resolve that question first; the other items (unsafe SAFETY comment, meta[0] assert, remove cfg_aliases, fix the typo) are straightforward fixups.


Automated review by Claude (Anthropic) · sonnet · custom prompt

@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR gates Shadow network simulator compatibility behind a shadow-integration feature flag, keeping the default production build unchanged while enabling a single-threaded tokio runtime and dropping jemalloc for Shadow builds. A local quinn-udp patch swapping GSO/GRO batch syscalls for plain send_to/recv_from is applied globally via [patch.crates-io] (since Cargo patches cannot be feature-gated), following the zeam project's shadow branch pattern.

  • Feature-gated changes (shadow-integration): current_thread tokio flavor, removal of jemalloc global allocator and malloc_conf export; a compile_error! prevents accidentally enabling both jemalloc and shadow-integration together.
  • Global patch (patches/quinn-udp): replaces quinn-udp with a plain-socket fallback (BATCH_SIZE = 1) that applies to every build on this branch, not just Shadow builds.
  • Infra changes: Dockerfile copies patches/ before cargo-chef graph resolution; Makefile gains shadow-build and docker-build-shadow targets.

Confidence Score: 3/5

The fallback recv path has an unguarded unwrap that can crash the node, and the global quinn-udp patch quietly removes batch UDP throughput from every build on this branch.

The fallback recv function calls addr.as_socket().unwrap() with no error handling — if the kernel returns an unexpected address family the node panics rather than logging an error. The global [patch.crates-io] also means anyone building from this branch gets degraded UDP performance without any compile-time or runtime signal.

patches/quinn-udp/src/fallback.rs warrants the closest look — it contains the unguarded unwrap and an undocumented unsafe layout cast. Cargo.toml is worth reviewing for the global-patch scope implications.

Important Files Changed

Filename Overview
patches/quinn-udp/src/fallback.rs New fallback UDP implementation for Shadow; contains an unguarded addr.as_socket().unwrap() that can panic at runtime, and an unsafe cast with no documented layout-safety justification.
patches/quinn-udp/src/lib.rs Public API surface for the patched quinn-udp crate; minor typo in a lock-panic message, otherwise structurally sound.
bin/ethlambda/src/main.rs Adds feature-gated allocator/runtime changes; compile_error! mutual-exclusion guard, conditional jemalloc, and current_thread tokio flavor for shadow builds — all clean.
Cargo.toml Adds a global [patch.crates-io] for quinn-udp; intentional but silently degrades UDP batch performance on all builds from this branch, not just shadow builds.
Dockerfile Copies patches/ before cargo-chef resolves the graph and adds NO_DEFAULT_FEATURES build arg — correct ordering and flag threading.
Makefile Adds shadow-build and docker-build-shadow targets with correct --no-default-features --features shadow-integration flags.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[cargo build] --> B{Features?}
    B -->|default| C[jemalloc allocator\nmulti-thread tokio]
    B -->|no-default-features + shadow-integration| D[system allocator\ncurrent_thread tokio]
    C --> E[quinn-udp fallback patch\nBATCH_SIZE=1\nno GSO/GRO]
    D --> F[quinn-udp fallback patch\nBATCH_SIZE=1\nno GSO/GRO]
    E --> G[Production binary\nUDP perf degraded on branch]
    F --> H[Shadow-compatible binary]
    I[compile_error!] -. blocks .-> J[jemalloc + shadow-integration combined]
Loading

Comments Outside Diff (1)

  1. Cargo.toml, line 83-90 (link)

    P2 quinn-udp patch silently degrades production UDP performance on this branch

    The [patch.crates-io] table is global — every cargo build --release on this branch (including CI/CD) will use the fallback send_to/recv_from path and get BATCH_SIZE = 1, forfeiting GSO/GRO batch throughput that the upstream quinn-udp provides. Anyone who pulls this branch and runs the default build will get a quietly degraded production binary without any warning at build time. The PR description mentions this tradeoff explicitly, but since there is no runtime warning or log message distinguishing a patched build from an upstream one, it could easily go unnoticed in the field.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: Cargo.toml
    Line: 83-90
    
    Comment:
    **quinn-udp patch silently degrades production UDP performance on this branch**
    
    The `[patch.crates-io]` table is global — every `cargo build --release` on this branch (including CI/CD) will use the fallback `send_to`/`recv_from` path and get `BATCH_SIZE = 1`, forfeiting GSO/GRO batch throughput that the upstream `quinn-udp` provides. Anyone who pulls this branch and runs the default build will get a quietly degraded production binary without any warning at build time. The PR description mentions this tradeoff explicitly, but since there is no runtime warning or log message distinguishing a patched build from an upstream one, it could easily go unnoticed in the field.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
patches/quinn-udp/src/fallback.rs:52
**Unguarded `unwrap` on received socket address**

`SockAddr::as_socket()` returns `None` for non-IPv4/IPv6 socket families (e.g. `AF_UNIX`, `AF_NETLINK`, abstract sockets). While UDP sockets should always produce IPv4/IPv6 addresses in practice, the `unwrap()` panics unconditionally if the kernel ever returns an unexpected address family — crashing the node process rather than surfacing an `io::Error`.

### Issue 2 of 4
patches/quinn-udp/src/lib.rs:81
Typo in the lock-poison message: "poisend" should be "poisoned".

```suggestion
    let last_send_error = &mut *last_send_error.lock().expect("poisoned lock");
```

### Issue 3 of 4
patches/quinn-udp/src/fallback.rs:44-47
**Unsafe cast missing `// SAFETY:` justification**

The cast from `*mut [IoSliceMut<'_>]` to `*mut [socket2::MaybeUninitSlice<'_>]` is sound only because both types are thin wrappers over `struct iovec` (on Unix) / `WSABUF` (on Windows) and therefore share identical memory layout. This invariant is implicit and not documented; a future Rust or `socket2` version could silently break it. A `// SAFETY:` comment explaining the layout equivalence assumption and citing relevant ABI guarantees would make the invariant auditable.

### Issue 4 of 4
Cargo.toml:83-90
**quinn-udp patch silently degrades production UDP performance on this branch**

The `[patch.crates-io]` table is global — every `cargo build --release` on this branch (including CI/CD) will use the fallback `send_to`/`recv_from` path and get `BATCH_SIZE = 1`, forfeiting GSO/GRO batch throughput that the upstream `quinn-udp` provides. Anyone who pulls this branch and runs the default build will get a quietly degraded production binary without any warning at build time. The PR description mentions this tradeoff explicitly, but since there is no runtime warning or log message distinguishing a patched build from an upstream one, it could easily go unnoticed in the field.

Reviews (1): Last reviewed commit: "feat: Shadow simulator compatibility beh..." | Re-trigger Greptile

meta[0] = RecvMeta {
len,
stride: len,
addr: addr.as_socket().unwrap(),

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.

P1 Unguarded unwrap on received socket address

SockAddr::as_socket() returns None for non-IPv4/IPv6 socket families (e.g. AF_UNIX, AF_NETLINK, abstract sockets). While UDP sockets should always produce IPv4/IPv6 addresses in practice, the unwrap() panics unconditionally if the kernel ever returns an unexpected address family — crashing the node process rather than surfacing an io::Error.

Prompt To Fix With AI
This is a comment left during a code review.
Path: patches/quinn-udp/src/fallback.rs
Line: 52

Comment:
**Unguarded `unwrap` on received socket address**

`SockAddr::as_socket()` returns `None` for non-IPv4/IPv6 socket families (e.g. `AF_UNIX`, `AF_NETLINK`, abstract sockets). While UDP sockets should always produce IPv4/IPv6 addresses in practice, the `unwrap()` panics unconditionally if the kernel ever returns an unexpected address family — crashing the node process rather than surfacing an `io::Error`.

How can I resolve this? If you propose a fix, please make it concise.

transmit: &Transmit,
) {
let now = Instant::now();
let last_send_error = &mut *last_send_error.lock().expect("poisend lock");

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.

P2 Typo in the lock-poison message: "poisend" should be "poisoned".

Suggested change
let last_send_error = &mut *last_send_error.lock().expect("poisend lock");
let last_send_error = &mut *last_send_error.lock().expect("poisoned lock");
Prompt To Fix With AI
This is a comment left during a code review.
Path: patches/quinn-udp/src/lib.rs
Line: 81

Comment:
Typo in the lock-poison message: "poisend" should be "poisoned".

```suggestion
    let last_send_error = &mut *last_send_error.lock().expect("poisoned lock");
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +44 to +47
) -> io::Result<usize> {
let bufs = unsafe {
&mut *(bufs as *mut [IoSliceMut<'_>] as *mut [socket2::MaybeUninitSlice<'_>])
};

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.

P2 Unsafe cast missing // SAFETY: justification

The cast from *mut [IoSliceMut<'_>] to *mut [socket2::MaybeUninitSlice<'_>] is sound only because both types are thin wrappers over struct iovec (on Unix) / WSABUF (on Windows) and therefore share identical memory layout. This invariant is implicit and not documented; a future Rust or socket2 version could silently break it. A // SAFETY: comment explaining the layout equivalence assumption and citing relevant ABI guarantees would make the invariant auditable.

Prompt To Fix With AI
This is a comment left during a code review.
Path: patches/quinn-udp/src/fallback.rs
Line: 44-47

Comment:
**Unsafe cast missing `// SAFETY:` justification**

The cast from `*mut [IoSliceMut<'_>]` to `*mut [socket2::MaybeUninitSlice<'_>]` is sound only because both types are thin wrappers over `struct iovec` (on Unix) / `WSABUF` (on Windows) and therefore share identical memory layout. This invariant is implicit and not documented; a future Rust or `socket2` version could silently break it. A `// SAFETY:` comment explaining the layout equivalence assumption and citing relevant ABI guarantees would make the invariant auditable.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@MegaRedHand MegaRedHand force-pushed the shadow-integration branch 6 times, most recently from 40f3383 to 387cb1d Compare June 8, 2026 18:15
Adds opt-in support for running ethlambda under the Shadow network
simulator (for the lean-shadow-fuzzer), gated so the default build, main,
and the committed Cargo.lock are unchanged.

jemalloc:
  Jemalloc causes programs to deadlock during process startup under
  Shadow (shadow/shadow#3763). The `jemalloc`
  feature (default-on) pulls the optional `tikv-jemallocator` dependency;
  the Shadow binary builds with `--no-default-features` to drop it
  entirely. A `compile_error!` catches enabling `shadow-integration`
  without `--no-default-features` (which would leave jemalloc linked).
  `jemalloc_pprof` stays: its `PROF_CTL` is `None` without the allocator,
  so the heap-profiling endpoints degrade gracefully.

runtime:
  Shadow single-steps execution, so a multi-threaded runtime only adds
  scheduling noise. The `current_thread` flavor is gated behind
  `shadow-integration`. This is an optimization, not a requirement.

quinn-udp:
  Shadow's UDP emulation lacks GSO/GRO batch syscalls, so quinn-udp must
  fall back to plain send_to/recv_from (see `shadow/quinn-udp-patch`). A
  Cargo `[patch]` cannot be feature-gated, so it is kept OUT of the
  committed manifest and injected at build time by `shadow/build.sh`
  (and the Dockerfile's SHADOW path). The committed Cargo.lock therefore
  stays identical to main.

Adds `make shadow-build` / `make shadow-docker-build`, the `shadow/`
directory (patch crate, injection script, README), plus `SHADOW`,
`NO_DEFAULT_FEATURES` and `LOCKED` Docker build args.

Ref: kamilsa/ethlambda@ed5a447
@MegaRedHand MegaRedHand force-pushed the shadow-integration branch from 387cb1d to 354d29a Compare June 8, 2026 18:23
Comment thread shadow/README.md Outdated
Comment thread shadow/README.md Outdated
Co-authored-by: Tomás Grüner <47506558+MegaRedHand@users.noreply.github.com>
@MegaRedHand MegaRedHand merged commit f5718ae into main Jun 8, 2026
2 of 3 checks passed
@MegaRedHand MegaRedHand deleted the shadow-integration branch June 8, 2026 19:17
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.

2 participants