Skip to content

sys: root the audit emit's livevars, and land the two _getframe commits #1112 merged without - #1132

Merged
youknowone merged 6 commits into
mainfrom
sys-audit-roots
Aug 10, 2026
Merged

sys: root the audit emit's livevars, and land the two _getframe commits #1112 merged without#1132
youknowone merged 6 commits into
mainfrom
sys-audit-roots

Conversation

@youknowone

@youknowone youknowone commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Follow-up to #1112, rebased onto current main.

#1112 was squash-merged at d2f4ce1522e, which predates part of the branch, so
one commit here is work that PR was supposed to carry and did not. The rest
answer the review comments the merged PR collected.

Not in main despite #1112

  • sys: force the frame _getframemodulename reads instead of the stack top
    main still forces topframeref at the walk START and reads w_globals off the
    frame the walk ENDS at without forcing it. w_globals is one of the six fields
    interp_jit.py:25-30 declares virtualizable, so the force belongs at the
    consumer; forcing the walk escapes the traced virtualizable and
    vable_after_residual_call aborts with ABORT_ESCAPE. Measured on the
    getframe_* corpus: vable-escape aborts 5 → 0 for depth ≥ 1.

The branch's second unmerged commit — unwrap and rebuild sys.audit's event
rather than forwarding the caller's object — has been dropped: #1113 landed
the same round trip meanwhile (str_utf8_w + w_str_new). See the note at the
bottom for the one thing that commit did differently.

Review answers

  • sys: root the audit emit's livevars ... — Codex P1 (reload pinned audit
    objects after collection points) and CodeRabbit's two vm.rs findings. Every
    value the audit path needs after an allocation now comes back through its
    shadow-stack slot; trigger_audit_events publishes the event, the arguments
    and the hook set in one phase and normalizes once, rather than pin_root per
    value (gc_roots::pin_roots documents why the per-value form reopens the
    window it exists to close). audit and sys_audit root their arguments in
    front of the event wrap, getframe roots the frame it returns across the
    emit, and sys_addaudithook pins before the sys.addaudithook event rather
    than after. The commit also moves the __cantrace__ lookup to
    findattr_result: space.findattr (baseobjspace.py:881-888) answers None
    for any non-async error, where the bare findattr panicked.
  • jit: bump the inline-subwalk close counter below the driver lookup
    CodeRabbit on state.rs.
  • bench: note which audit-hook arms are unreachable ... — CodeRabbit's three
    fixture comments. The requested extra coverage for the propagating arm of the
    refusal predicate is not added, and the header now records why it cannot
    be: a hook is installed for the life of the interpreter and the first one that
    raises masks every hook behind it, and upstream's own facility for clearing the
    set between cases (__pypy__._testing_clear_audithooks,
    interp_magic.py:292) refuses to run once translated.
  • bench: retarget the audit-hook note at error_is_exception`` — the same
    header cited error_is_runtime_error and `RuntimeError`, both of which coroutine, generator, io, exceptions and CPython allocation/layout metadata; five jit fixes #1126
    replaced (`Exception`, renamed predicate).

One review finding deliberately not acted on

Codex P1 on residual_call.rs:1356 asks for mark_escape_flush_undo_pending()
back in the locals-only escape-flush arm. That arm was removed by
jit: drop the locals-region leg's deferred escape-flush undo arm on measurement:
the value-stack underflow it was added for came from an A/B run on stale LLBC,
and re-running the reproducing arm on artefacts that pass PYRE_LLBC_STRICT=1
gives 0/10 panics on cranelift with the whole synthetic suite and every recorded
counter unmoved. Restoring the call would revert an executed result on a static
argument, so this PR fixes the comment instead — it still claimed the capture
was consumed by a legacy replay, which is what stopped being true.

One open question, left as main has it

sys_audit unwraps with str_utf8_w, and #1113's comment states that choice is
deliberate: "an event name holding a lone surrogate is a UnicodeEncodeError at
the call". The dropped commit used the surrogate-tolerant read instead. Measured:

pypy3 7.3.22    sys.audit("\ud800bad")  → accepted, the hook receives '\ud800bad'
CPython 3.14.6  sys.audit("\ud800bad")  → UnicodeEncodeError

Upstream's spec is @unwrap_spec(event="text"), i.e. space.text_w, and the
PyPy oracle accepts the name. This PR does not change it — reversing a
documented, deliberate decision is not something a conflict resolution should
do silently. Raising it here so the call is made on the evidence rather than by
whichever branch merged last.

Verification

PYRE_LLBC_STRICT=1 python3 pyre/check.py --synthetic-only:

FAILED: dynasm 1 failed, 394 passed
FAILED: cranelift 1 failed, 394 passed
ALL PASSED: wasm 394/394

The single failure is synth/list_pop_append, an inherited base red: the CI run
for #1119's own merge commit (1a63e36e37a, run 31313268003, headSha equal)
already reads FAIL dynasm 59.8x > gate 22x / FAIL cranelift 68.9x > gate 22x
on the fixture that same commit introduced the 22x ceiling for, and every main
run since carries it.

Output parity against pypy3 re-checked for sys_audit_hooks,
getframe_audit_event, getframe_inline_subwalk_multiframe and
getframe_while_escaping_read_frame_identity. cargo fmt --check clean.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The interpreter now roots frames, audit events, arguments, and hooks across collection points. Frame lookup delays forcing until globals are read. JIT diagnostic counting and benchmark documentation also receive targeted updates.

Changes

Frame and audit rooting

Layer / File(s) Summary
Rooted audit dispatch
pyre/pyre-interpreter/src/module/sys/vm.rs
Audit dispatch roots event data and hooks, validates WTF-8 event text, and handles __cantrace__ lookup errors. addaudithook roots the new hook during notification.
Rooted frame access
pyre/pyre-interpreter/src/module/sys/vm.rs
getframe roots the returned frame during audit dispatch. Frame-module lookup forces only the frame used for w_globals.
JIT diagnostics and benchmark annotations
pyre/pyre-jit-trace/src/state.rs, pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs, pyre/bench/synth/*
Driverless inline subwalk ends no longer increment diagnostic counter 59. Comments describe flush behavior, adoption counts, audit-hook failures, and an intentional frame-local read.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PythonProgram
  participant sys_audit
  participant AuditDispatch
  participant AuditHook
  PythonProgram->>sys_audit: submit event and arguments
  sys_audit->>AuditDispatch: create rooted event and arguments
  AuditDispatch->>AuditHook: invoke rooted hook
Loading

Possibly related PRs

Poem

A rabbit roots each frame with care,
While audit hooks hop through air.
The JIT counts only drivers near,
Comments make each pathway clear.
sys.audit safely bounds the snare.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title identifies the main audit-rooting change and the related _getframe commits, although its ending is grammatically incomplete.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sys-audit-roots

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyre/pyre-interpreter/src/module/sys/vm.rs`:
- Around line 2584-2599: Run and report both cargo check and cargo test with the
dynasm feature, plus all eight benchmarks because the JIT files changed. Include
the results in the PR description and explain any benchmark regressions without
reverting parity-correct changes.
- Around line 2603-2604: Change the audit-hook function’s holder parameter from
&AuditHolder to *const AuditHolder so no shared reference remains live during
raw writes. Within the function, dereference the pointer only as needed to call
publish_roots on hooks_w and copy hook_count before any hook can mutate the
holder through sys.addaudithook. Update affected call sites to pass the raw
pointer while preserving existing hook behavior.
- Around line 2736-2740: Update sys_audit to call split_builtin_kwargs(args)
before pinning or constructing args_w, and reject any real keyword arguments
while retaining the positional arguments only. Build args_w from the stripped
argument slice so the trailing __pyre_kw__ marker is never forwarded as an audit
argument.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c85b0c89-23ed-4670-8c3c-6feb63330026

📥 Commits

Reviewing files that changed from the base of the PR and between 8e3ed93 and adfe74d.

📒 Files selected for processing (6)
  • pyre/bench/synth/getframe_inline_subwalk_multiframe.py
  • pyre/bench/synth/getframe_while_escaping_read_frame_identity.py
  • pyre/bench/synth/sys_audit_hooks.py
  • pyre/pyre-interpreter/src/module/sys/vm.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-jit-trace/src/state.rs

Comment on lines +2584 to +2599
// `pin_root` copies the pointer into a shadow-stack slot and the collector
// rewrites THAT slot, never the local it was copied from. So every value
// still needed after one of the app-level calls below is reached through
// its slot index, the way `baseobjspace::isinstance` does it.
//
// The event, the arguments and the hook set are one livevar set spanning
// three slices, so they are published together and normalized once
// (`gc_roots::pin_roots`): a per-value pin queries the collector after the
// first write, which would let a foreign collection run while the values
// behind it were still invisible to it.
//
// A hook may install another hook, and upstream's list is replaced rather
// than appended to, so an in-flight trigger keeps iterating the set it
// started with. Snapshotting into pinned roots reproduces that and keeps
// every callable forwarded across the calls below.
let hooks_w = holder.hooks_w.clone();
for &w_hook in &hooks_w {
pyre_object::gc_roots::pin_root(w_hook);
}
// started with. The published slots ARE that snapshot — they keep every
// callable forwarded across the calls below and are unaffected by a
// replacement of `holder.hooks_w`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Report the required build and benchmark runs.

The PR description reports cargo fmt --check and the wasm synthetic suite. It does not report cargo check and cargo test with --features dynasm. The stack also changes pyre/pyre-jit-trace/src/state.rs and pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs, so the eight-benchmark run applies. Add both results, and explain any regression instead of reverting parity-correct code.

As per coding guidelines: "Before committing, run cargo check and cargo test with --features dynasm; after JIT changes, run all eight benchmarks and explain regressions rather than automatically reverting parity-correct code."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-interpreter/src/module/sys/vm.rs` around lines 2584 - 2599, Run and
report both cargo check and cargo test with the dynasm feature, plus all eight
benchmarks because the JIT files changed. Include the results in the PR
description and explain any benchmark regressions without reverting
parity-correct changes.

Source: Coding guidelines

Comment on lines +2603 to +2604
let hooks_slot = pyre_object::gc_roots::publish_roots(&holder.hooks_w);
let hook_count = holder.hooks_w.len();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider taking the holder as a raw pointer instead of &AuditHolder.

The holder shared reference stays live for the whole function. An audit hook can call sys.addaudithook, which writes (*holder).hooks_w = next through a raw pointer at line 2789 while this shared reference is alive. The changed code no longer reads holder after line 2604, so behavior is correct, but the overlapping shared reference and raw write is an aliasing violation under Stacked Borrows. Copy hook_count and publish the hooks, then drop the reference by taking *const AuditHolder in the signature.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-interpreter/src/module/sys/vm.rs` around lines 2603 - 2604, Change
the audit-hook function’s holder parameter from &AuditHolder to *const
AuditHolder so no shared reference remains live during raw writes. Within the
function, dereference the pointer only as needed to call publish_roots on
hooks_w and copy hook_count before any hook can mutate the holder through
sys.addaudithook. Update affected call sites to pass the raw pointer while
preserving existing hook behavior.

Comment thread pyre/pyre-interpreter/src/module/sys/vm.rs Outdated
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 9f2df15).
Updated: 2026-08-10T01:44:20.771Z

Files in the reviewed diff
pyre/bench/synth/getframe_inline_subwalk_multiframe.py
pyre/bench/synth/getframe_while_escaping_read_frame_identity.py
pyre/bench/synth/sys_audit_hooks.py
pyre/pyre-interpreter/src/module/sys/vm.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
pyre/pyre-jit-trace/src/state.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • pyre/pyre-interpreter/src/module/sys/vm.rs:2698-2704 ↔ pypy/interpreter/baseobjspace.py:881-888: the new __cantrace__ lookup propagates only SystemExit and suppresses KeyboardInterrupt; PyPy’s findattr propagates every asynchronous exception. A hook whose __cantrace__ raises KeyboardInterrupt is therefore called in pyre but must abort audit dispatch in PyPy.

3. Pre-existing mismatches (already present before this patch)

  • pyre/pyre-interpreter/src/baseobjspace.rs:4259-4275 ↔ pypy/interpreter/baseobjspace.py:881-888: the existing findattr panics for ordinary lookup errors other than AttributeError/NameError, whereas PyPy suppresses all non-async OperationErrors and returns None. The patch avoids this helper for audit hooks, but does not correct the general helper.

4. Structural adaptations

  • pyre/pyre-interpreter/src/module/sys/vm.rs:2852-2856 ↔ pypy/module/sys/vm.py:493-499: pyre deliberately treats any Exception from sys.addaudithook as silent refusal, while this local PyPy source suppresses only RuntimeError. This is the stated Python 3.14 API adaptation, not an RPython-port defect.

  • pyre/pyre-interpreter/src/module/sys/vm.rs:2669-2682 ↔ pypy/module/sys/vm.py:447-450: explicit shadow-stack publication and reloading replace PyPy’s GC-visible object-space locals when constructing the event tuple and snapshotting hooks. This is required by Rust’s movable-GC/raw-pointer representation and preserves PyPy’s tuple-before-iteration and snapshot semantics.

  • pyre/pyre-interpreter/src/module/sys/vm.rs:1312-1319 ↔ rpython/jit/codewriter/jtransform.py:2164-2172: pyre explicitly forces the frame immediately before reading w_globals; RPython’s virtualizable lowering handles/removes the corresponding force through generated JIT operations. This is a Rust/JIT-pipeline adaptation.

  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs:1355-1382 ↔ rpython/jit/metainterp/virtualizable.py:231-255: pyre’s frame-flush undo bookkeeping has no RPython counterpart; removing the deferred restore leaves the force outcome governed by the virtualizable token, matching upstream’s force_now / tracing_after_residual_call contract.

  • pyre/pyre-jit-trace/src/state.rs:3286-3296 ↔ rpython/jit/metainterp/pyjitpl.py:2494-2496: moving diagnostic counter 59 below the driver lookup is pyre-only accounting around the manually synthesized inline-subwalk close; PyPy appends directly from an MIFrame pop and has no equivalent counter.

… top

`_getframemodulename` forced `topframeref` before walking and then read
`w_globals` off the frame the walk ends at, unforced. `w_globals` is one of the
six fields `interp_jit.py:25-30` declares virtualizable, so the read is the
consumer that needs the force; forcing at the walk instead escapes the traced
virtualizable and `vable_after_residual_call` aborts the trace, which is the
shape `executioncontext.rs force_frame` documents.

Measured on a three-deep call chain over 100000 iterations: `abort: vable
escape` goes 5 -> 0 at depths 1, 2 and 3, and stays 5 at depth 0, where the
frame being read is the live portal virtualizable. Reported module names are
unchanged at depths 0-4 and match a `PYRE_NO_JIT=1` run.

Assisted-by: Claude
… with None

`pin_root` writes a shadow-stack slot and a moving collection rewrites that slot,
not the local it was copied from, so every value the audit path still needs after
an allocation is now read back through its slot index:

  * `trigger_audit_events` publishes the event, the caller's arguments and the
    hook set with `publish_roots` and normalizes the whole range once, rather
    than calling `pin_root` per value — a per-value pin queries the collector
    after the first write, leaving the values behind it invisible to a foreign
    collection (`gc_roots::pin_roots`). The tuple, the event and each hook then
    come out of `shadow_stack_get` at every use.
  * `audit` roots `args_w` before `w_str_new` and emits from the reloaded
    values; `sys_audit` does the same around `w_str_from_wtf8`. The slice a
    builtin receives is `call_function_impl_result`'s reloaded copy on the Rust
    stack, which no collection rewrites.
  * `getframe` roots the frame it is about to return across the emit and reads
    it back, and enters that bracket only when `audit_hooks_armed()`.
  * `sys_addaudithook` pins before the `sys.addaudithook` event instead of
    after it, and takes the hook out of its slot when appending; the borrow of
    `holder.hooks_w` is scoped so none is live across the `Vec` allocation.

The `__cantrace__` lookup moves from `baseobjspace::findattr` to
`findattr_result`. `space.findattr` (`baseobjspace.py:881-888`) returns `None`
for any non-async error out of the lookup, so a hook whose `__cantrace__`
descriptor raises is treated as not having one; the bare `findattr` panics on
those instead. SystemExit is the arm that travels out.

Assisted-by: Claude
The note still claimed "the undo stays armed so the legacy replay re-enters the
pre-flush frame" after the deferred arm was withdrawn. The capture does stay in
`ESCAPE_FLUSH_UNDO`, but nothing consumes it from this arm: the committed-pc
walk-end leg is gated on a pc this arm never sets and the deferred leg on a flag
it never arms, so `capture_escape_flush_undo` supersedes it on the next force or
the walk-start reset drops it.

Assisted-by: Claude
`note_inline_subwalk_end` bumped 59 before `try_driver_pair`, so a driverless
call counted a close that never pushed an entry. It now bumps where
`note_inline_subwalk_start` bumps 58 — both counters measure an appended entry,
and an unclosed entry reads as `ptp_push != ptp_pop`.

Assisted-by: Claude
`sys_audit_hooks` records why the non-RuntimeError and RuntimeError-subclass
arms of `error_is_runtime_error` are not exercised and cannot be from app level:
a hook is installed for the life of the interpreter and the first one that
raises masks every hook behind it, and the upstream facility for clearing the
set between cases (`__pypy__._testing_clear_audithooks`, `interp_magic.py:292`)
refuses to run once translated.

`getframe_while_escaping_read_frame_identity` states the two counts separately —
the multi-frame adoption count is unmoved at 10, and it is the single-frame
count that went 5 -> 0 alongside `part_a`'s loop compiling.

`getframe_inline_subwalk_multiframe` carries `# noqa: B018` on the standalone
`sys._getframe(0).f_locals`, whose read IS the force under test.

Assisted-by: Claude
The refusal predicate widened from `RuntimeError` to `Exception` and was renamed,
so the header named a function and a class that no longer decide anything here.
It now states which arm the app-level refusal below takes — a raise from app code
carries an exception object, so the isinstance arm answers and the `PyErrorKind`
fallback behind it is for errors that never materialised one.

Assisted-by: Claude

@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

https://github.com/youknowone/pyre/blob/9f2df156936ee52b79db965fbb8bd9645e04fd36/pyre-interpreter/src/module/sys/vm.rs#L2704
P2 Badge Propagate KeyboardInterrupt from cantrace lookup

When an audit hook's __cantrace__ descriptor raises KeyboardInterrupt, PyError::from_exc_object represents it with PyErrorKind::RuntimeError while retaining the actual exception object, so the SystemExit-only guard misses it and this catch-all silently treats the attribute as absent. PyPy's space.findattr re-raises both SystemExit and KeyboardInterrupt, meaning sys.audit should propagate the interruption rather than proceeding to invoke the hook; check the materialized exception object with the shared async-exception logic before swallowing the error.

AGENTS.md reference: AGENTS.md:L231-L233

ℹ️ 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".

@youknowone

Copy link
Copy Markdown
Owner Author

CI attribution for this PR — every failing row is inherited from main or a measurement artifact, verified against main @ eba122397df (run 31346388732), the same sha this branch sits on.

job main @ eba1223 this PR verdict
pyre/check.py (ubuntu) FAIL wasm synth/unary_negative jit-stats change — improved: guard_failures 13 -> 2 byte-identical row inherited
pyre/check.py (windows) synth 414/414 pass; job fails on posix parity (access() requires 2 arguments, utime(bytearray), No module named 'posix') identical inherited
pyre/check.py (macos) pass synth/list_setslice 0.1x < floor 0.2x measurement artifact, see below
cargo test (ubuntu) pass passed on re-run (was a memory allocation of 8 bytes failed SIGABRT in pyre-object --lib, a crate this branch does not touch) runner flake
CPython suite (gate) pass pass

unary_negative — the recorded baselines are dynasm=2, cranelift=2, wasm=13; the new reading of 2 makes wasm agree with the natives. It reproduces on main itself, so the re-record belongs with whichever change moved it, not here. A local A/B pins the window: the same branch code read ALL PASSED: wasm 394/394 on base 1b31a5b3060 and produces this row on eba122397df, same host — the only variable is main.

list_setslice — read the two runs' own startup lines:

main  macOS   cpython=0.016s pypy=0.017s dynasm=0.056s cranelift=0.053s   -> 0.4x / 0.5x, pass
this  macOS   cpython=0.020s pypy=0.021s dynasm=0.080s cranelift=0.070s   -> 0.1x / 0.1x, fail

The whole host was ~20% slower and dynasm startup went 0.056s -> 0.080s, while the fixture's total stayed 0.07-0.08s on both. exec = total - startup therefore goes negative and clamps to the 0.01s floor, which is the entire 0.1x. Locally the same fixture reads 0.5-0.6x, 3/3. This is the floor side of the denominator problem #1121 buffered on the ceiling side.

No jitstats or ceiling has been re-recorded in this PR.

commented by Claude

@youknowone
youknowone merged commit 74a298d into main Aug 10, 2026
24 of 29 checks passed
@youknowone
youknowone deleted the sys-audit-roots branch August 10, 2026 08:31
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