Skip to content

[AAASM-3569] 🔒 (aa-sdk-client): SDK↔runtime IPC authentication & anti-impersonation#1216

Merged
Chisanan232 merged 27 commits into
masterfrom
v0.0.1/AAASM-3569/sdk_runtime_ipc_auth
Jun 24, 2026
Merged

[AAASM-3569] 🔒 (aa-sdk-client): SDK↔runtime IPC authentication & anti-impersonation#1216
Chisanan232 merged 27 commits into
masterfrom
v0.0.1/AAASM-3569/sdk_runtime_ipc_auth

Conversation

@Chisanan232

Copy link
Copy Markdown
Contributor

Description

Closes Story AAASM-3569 (under Epic AAASM-3567): authenticates the local SDK↔aa-runtime UDS channel and the gateway credential_token mint, closing the local-impersonation and forged-event vectors.

Three controls, layered:

  • UDS peer-credential check (AAASM-3579): the runtime reads SO_PEERCRED/getpeereid via UnixStream::peer_cred in the accept loop and drops any connection whose process UID ≠ the runtime's euid, before any handler is spawned.
  • Atomic 0600 socket + agent-scoped path (AAASM-3581): umask(0o077) around bind removes the bind→chmod TOCTOU window; the socket path is scoped to AA_AGENT_ID.
  • Per-session Ed25519 handshake (AAASM-3583/3585/3587): a new assembly.ipc.v1 proto (HandshakeChallenge/HandshakeProof) + new wire tags (=5). On accept the runtime sends a fresh 32-byte nonce and requires the SDK's Ed25519 signature over it, verified against the key deterministically derived from AA_AGENT_ID (reusing AgentKeypair). The handshake runs in the per-connection task (never blocks the accept loop), is bounded by a 5s timeout, and the response router / active-conn counter are wired only after a valid proof. The SDK completes the handshake before any heartbeat/event, fail-closed.
  • Credential_token possession gate (AAASM-3591): gateway Register now requires an Ed25519 possession_proof over the registering did:key, verified against public_key, before generate_credential_token(). Missing/forged/wrong-length → Unauthenticated. The SDK signs and sends it. Coordinates with AAASM-3416 (broad per-endpoint authn, still To Do) — this is the minimal credential_token possession gate that a future authn interceptor layers on top of, not a replacement.

Type of Change

  • ✨ New feature
  • 🔧 Configuration / CI change

Breaking Changes

  • Yes (describe below)

RegisterRequest gains a required possession_proof field (proto field 14): a gRPC Register call without a valid proof now returns Unauthenticated instead of minting a token. aa-sdk-client::ipc::spawn_ipc_thread / ipc_loop now take an agent_id: String to derive the handshake signing key. The cross-repo FFI shims (python/node/go SDKs) consume aa-sdk-client by pinned SHA and will adopt these on their own cadence.

Related Issues

  • Related Jira ticket: AAASM-3569 (subtasks AAASM-3579/3581/3583/3585/3587/3589/3591/3592)
  • Coordinates with: AAASM-3416 (gateway per-endpoint authn — credential_token possession gate added here, interceptor deferred to that Story)

Testing

  • Unit tests added / updated
  • Integration tests added / updated

Local validation (CI verifies authoritatively):

  • cargo nextest run -p aa-sdk-client -p aa-runtime -p aa-gateway -p conformance → 1575 passed, 2 skipped.
  • New tests: peer-UID allow helper; 0600 + agent-scoped bind; handshake nonce/proof verify (valid/forged/wrong-nonce/wrong-key/bad-length); handshake codec round-trip; impersonation/forged-event rejection (no-handshake event not dispatched, forged-sig rejected, valid handshake dispatches); gateway possession-proof accept/missing/forged/wrong-challenge/wrong-length; SDK AgentKeypair::sign.
  • cargo clippy --all-targets --all-features -- -D warnings clean on touched crates; cargo fmt --check clean; cargo build --workspace clean.

Platform note: SO_PEERCRED is Linux-specific; the peer-UID read uses tokio's portable peer_cred() (getpeereid on macOS/BSD), so the path compiles and runs on all Unix targets. Tests were authored and run on macOS; the exact UID-mismatch reject is best validated under Linux CI (a same-host different-UID peer is hard to stage in a unit test) — the helper that decides it is unit-tested directly.

Checklist

  • Code follows project style guidelines (cargo fmt, cargo clippy)
  • Self-review of the diff completed
  • Documentation updated if behaviour changed (doc comments on all new public items)
  • All CI checks passing (pending CI)
  • Commits are small and follow the Gitmoji convention

🤖 Generated with Claude Code

Chisanan232 and others added 21 commits June 23, 2026 20:08
Move libc to a cfg(unix) target table so geteuid() is available on
macOS/BSD as well as Linux, backing the IPC socket peer-UID check.

Refs AAASM-3579.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure peer_uid_is_allowed() policy + current_runtime_uid() (geteuid),
isolated and unit-tested so the platform-specific code is testable
without opening a real socket.

Refs AAASM-3579.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read SO_PEERCRED/getpeereid via UnixStream::peer_cred in the accept
loop and drop any connection whose process UID does not match the
runtime's euid, before spawning any handler — defence-in-depth over
the 0600 socket perms against local impersonation/forged events.

Closes AAASM-3579.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apply umask(0o077) around UnixListener::bind so the socket inode is
owner-only from creation — closes the bind→chmod TOCTOU window. Add
agent_id to IpcServerConfig and debug-assert the bound path is scoped
to it. Keep set_permissions(0o600) as belt-and-suspenders.

Refs AAASM-3581.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Asserts the socket file mode is exactly 0o600 immediately after bind
and the path contains the configured agent id.

Closes AAASM-3581.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HandshakeChallenge{nonce} + HandshakeProof{agent_did, public_key,
signature} for the local SDK↔runtime UDS session handshake; wired into
build.rs codegen and the lib.rs module tree.

Refs AAASM-3583.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
IpcFrame::HandshakeProof (inbound) + IpcResponse::HandshakeChallenge
(outbound) wrapping the assembly.ipc.v1 protos for the per-session UDS
handshake.

Refs AAASM-3583.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ests

Add TAG_HANDSHAKE_PROOF=5 (inbound) / TAG_HANDSHAKE_CHALLENGE=5
(outbound), extend read_frame/write_response for the new handshake
frames (existing tags unchanged), and add round-trip tests:
HandshakeProof decodes through read_frame; HandshakeChallenge encodes
through write_response with the correct tag.

Closes AAASM-3583.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dependencies for verifying the SDK's Ed25519 proof of key possession
and generating per-session challenge nonces.

Refs AAASM-3585.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
expected_verifying_key() derives the agent's key from AA_AGENT_ID the
same way the SDK does; generate_nonce() yields a fresh 32-byte nonce;
verify_proof() fails closed unless the proof's public_key matches the
expected key and the signature verifies over the exact nonce. Unit
tests cover valid/forged/wrong-nonce/wrong-key/bad-length proofs.

Refs AAASM-3585.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Before any frame is dispatched, the runtime challenges each accepted
connection with a fresh nonce (perform_handshake) and verifies the
SDK's signed proof against the key derived from AA_AGENT_ID. The
handshake runs inside the per-connection task (never the accept loop),
bounded by a 5s timeout; the response router and active-connection
counter are wired only after a valid proof, so an unauthenticated or
non-handshaking peer is dropped without dispatch. Existing dispatch
tests updated to perform the SDK-side handshake.

Refs AAASM-3585.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retain the SigningKey and add sign(message) -> [u8;64] so the SDK can
prove key possession over the runtime's handshake nonce. Tests assert
the signature verifies under the public key and is deterministic.

Refs AAASM-3587.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rite)

read_handshake_challenge() returns the runtime's nonce;
write_handshake_proof() sends the signed proof. Tags mirror aa-runtime
(TAG_HANDSHAKE_CHALLENGE/PROOF=5).

Refs AAASM-3587.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…osed

ipc_loop now reads the runtime's nonce challenge and sends a signed
HandshakeProof (perform_handshake/build_handshake_proof) before any
heartbeat/event; a failed handshake aborts the loop rather than leaking
traffic onto an unauthenticated channel. spawn_ipc_thread/ipc_loop take
the agent_id to derive the signing key. Mock-server tests + the
lifecycle e2e updated to perform the server-side handshake.

Closes AAASM-3587.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three black-box cases on the gated IPC server: (1) a peer that sends an
EventReport without handshaking is never dispatched; (2) a peer whose
handshake signature is corrupted is rejected and never dispatched;
(3) a peer with a valid handshake has its event dispatched.

Closes AAASM-3589.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Field 14: a raw Ed25519 signature over the registering did:key proving
the caller holds the private key for public_key, gating credential_token
issuance (AAASM-3591).

Refs AAASM-3591.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cargo.lock update for the aa-runtime handshake-verification dependencies
added in this branch.

Refs AAASM-3585.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Register now verifies an Ed25519 possession_proof over the registering
did:key against public_key (verify_possession_proof) and returns
Unauthenticated when it is missing/forged/wrong-length — closing the
"hit unauthenticated :50051 and mint a credential_token" path. Unit
tests cover accept/missing/forged/wrong-challenge/wrong-length.
Coordinates with AAASM-3416; minimal gate a future interceptor layers on.

Refs AAASM-3591.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The exhaustive RegisterRequest literal in the serialization test now
includes the new possession_proof field (AAASM-3591).

Refs AAASM-3591.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build_register_request signs the registering did:key with the agent's
keypair and sets possession_proof, so the gateway admits the
registration and mints a credential_token.

Refs AAASM-3591.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Success-path Register requests now sign their did:key to pass the new
possession gate; the parent-unknown test gets a valid proof so it still
reaches the InvalidArgument parent-lookup. Invalid-public-key and
non-did:key tests left unproofed — they fail earlier by design.

Closes AAASM-3591.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread aa-runtime/src/ipc/handshake.rs Fixed
Chisanan232 and others added 2 commits June 23, 2026 21:59
The runtime↔gateway e2e harness needs the production AgentKeypair to sign
the AAASM-3585 session handshake nonce; pull aa-sdk-client into dev-deps so
the test can reuse the real signer instead of duplicating Ed25519 crypto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…teway e2e

AAASM-3585 made the SDK↔runtime IPC require a signed per-session handshake
before the runtime serves any PolicyQuery: on connect the runtime sends a
HandshakeChallenge{nonce} and expects a HandshakeProof signed with the
agent's deterministic Ed25519 key. This test connected and sent a PolicyQuery
without handshaking, so the runtime's first frame was the challenge (tag 5),
not a PolicyResponse (tag 1), and the assertion at line 191 failed.

Make check_tool complete the handshake first — read the challenge, sign the
nonce with the production aa_sdk_client::AgentKeypair (seed = SHA-256(agent_id),
identical to the runtime's expected key), and send the proof — mirroring the
real SDK exactly. The runtime's security behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Chisanan232

Copy link
Copy Markdown
Contributor Author

CI fix: e2e_runtime_gateway_deny broken by the new IPC handshake

The Test job (run 28027853201) failed on aa-integration-tests::e2e_runtime_gateway_deny::runtime_forwards_per_tool_deny_to_gateway at e2e_runtime_gateway_deny.rs:191assertion left == right failed: expected a PolicyResponse frame.

Root cause (test-harness gap, not a runtime bug). This PR's AAASM-3585 change makes the runtime require a signed per-session Ed25519 handshake before serving any application frame: on connect the runtime sends a HandshakeChallenge{nonce} (tag 5) and expects a HandshakeProof signed with the agent's deterministic key. The e2e test connected to the runtime UDS and immediately sent a PolicyQuery without handshaking, so the runtime's first frame was the challenge (tag 5), which the test misread as the response tag and asserted 5 == 1. Reproduced locally exactly: left: 5, right: 1.

The runtime is correct — verified the slipped test was the only one in aa-integration-tests that drives the IPC directly (the other tests that touch aa-runtime use the AuditPublisher, the gRPC path, or just probe UDS connectivity, none of which the handshake breaks). It slipped because the implementing pass validated -p aa-sdk-client -p aa-runtime -p aa-gateway -p conformance only, not aa-integration-tests.

Fix. Made the test harness perform the handshake before sending the query, mirroring the real SDK — read the challenge, sign the nonce with the production aa_sdk_client::AgentKeypair (seed = SHA-256(agent_id), the exact key the runtime expects), send the proof. Reuses the real signer rather than duplicating crypto, so no security behaviour is weakened.

  • 5cc6a431 🔧 add aa-sdk-client dev-dep
  • 95e71937 🐛 perform the IPC Ed25519 handshake in check_tool

Local proof (macOS, stable toolchain):

  • target test: FAIL without the fix (left: 5, right: 1), PASS with the fix
  • cargo build --workspace green; cargo clippy -p aa-integration-tests --all-targets -- -D warnings clean; cargo fmt --check clean

— Claude Code

@Chisanan232

Copy link
Copy Markdown
Contributor Author

Review — AAASM-3569 SDK↔runtime IPC authentication & anti-impersonation

Verdict: READY to merge (pending the required Pioneer approval — no code/CI blocker).

CI

Authoritative ci.yml run is completed / success at PR head 95e71937; the CI Success gate is green. All Rust/security jobs pass: Test, Clippy, Format, Rustdoc, Rust conformance, TimescaleDB, Integration tests (macOS), Analyze (rust/python/go/js-ts), no_std targets, Standalone build, Proto breaking check.

The four red checks are not code failures / not gating:

  • Docker + Docker image smoke (aa-runtime sidecar, multi-arch build, node image) — all three images build successfully (every layer DONE); they fail only at the exporting to GitHub Actions Cache step with a transient 503 upstream connect error from GitHub's cache backend. Pure infra flake, unrelated to this PR — these same workflows are green on master.
  • CodeQL — 3s default-setup/QUALITY-mode feed artifact; the real Analyze (*) jobs all passed.

No fix pushed (would be wrong to "fix" an infra flake with code).

Scope coverage — all 8 subtasks verified

  • 3579 SO_PEERCRED peer-UID reject — ipc/peercred.rs + accept-loop peer_cred() gate (rejects mismatched UID and unreadable creds before any work). ✅
  • 3581 atomic 0600 socket scoped to AA_AGENT_ID — umask(0o077) around bind closes the prior bind→chmod TOCTOU window; agent-id path scoping asserted; covered by bind_creates_socket_with_0600_and_agent_scoped_path. ✅
  • 3583 per-session handshake frame + nonce — ipc.proto HandshakeChallenge{nonce} / HandshakeProof, fresh random nonce per session (documented replay protection). ✅
  • 3585 runtime Ed25519 verify gating, fail-closed — perform_handshake runs inside the spawned task (never stalls accept loop), 5s timeout, response-router + active-conn counter wired only after a valid proof. ✅
  • 3587 SDK signed proof on connect — aa-sdk-client::ipc::perform_handshake signs the nonce before any traffic and aborts the loop on failure (fail-closed); AgentKeypair::sign added. ✅
  • 3591 / coord. 3416 credential_token possession gate — verify_possession_proof in lifecycle_service::Register; missing/malformed/forged proof → Unauthenticated, no token minted. ✅
  • 3589 impersonation/forged-event tests — fail-closed proven: event_without_handshake_is_not_dispatched, forged_handshake_signature_is_rejected (both assert no inbound dispatch), plus valid_handshake_then_event_is_dispatched happy path; e2e in e2e_runtime_gateway_deny.rs. ✅
  • 3592 verify — gateway lifecycle tests + conformance round-trip set possession_proof. ✅

Commit history is granular and bisectable (23 commits, one logical unit each).

⚠️ Breaking change (coordination note, not a blocker)

RegisterRequest.possession_proof (field 14) is now required: an unauthenticated Register (empty proof) returns Unauthenticated. The python / node / go FFI shims will fail to register on their next aa-sdk-client SHA-pin bump unless they send the proof. The SDK-side change is included here (aa-sdk-client signs did:key for Register and signs the nonce for the IPC handshake), so the contract is consistent within this repo — but the downstream SDK repos must pick it up in lockstep with the pin bump.

Design note (non-blocking)

The handshake keypair is derived deterministically from SHA-256(agent_id), and agent_id is not secret — so a local process that knows the agent_id can reconstruct the signing key and produce a valid proof. The handshake is therefore defence-in-depth (makes the trust boundary explicit + testable), not an independent secret; the real local-impersonation boundary is the SO_PEERCRED UID check + 0600 socket. The code documents this honestly. Future hardening (a real per-agent secret) can layer on without changing the wire shape.

— Claude Code

Chisanan232 and others added 3 commits June 24, 2026 08:15
The codec round-trip test built a HandshakeChallenge from a hard-coded
literal nonce (vec![3u8; 32]), which CodeQL flags as a hard-coded value
used as a nonce. Generate a fresh random nonce via the production
handshake::generate_nonce() and assert the decoded value round-trips
against it. Production nonce generation is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nces

The mock-server handshake tests need a CSPRNG to generate fresh per-test
challenge nonces instead of embedding hard-coded literals (CodeQL
hard-coded-nonce alert). getrandom 0.2 mirrors the runtime's production
nonce source and is already resolved in the lockfile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The mock-server handshake helpers in ipc.rs and lifecycle_e2e.rs built
HandshakeChallenge from hard-coded literal nonces (vec![42u8; 32] /
vec![9u8; 32]), which CodeQL flags as hard-coded values used as a nonce.
Fill a fresh 32-byte nonce from getrandom each run; the proof is still
signed over and verified against the random nonce, so the assertions are
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Chisanan232

Copy link
Copy Markdown
Contributor Author

Cleared the CodeQL hard-coded-nonce alert

The CodeQL "critical" finding ("This hard-coded value is used as a nonce", ×6) was not in production. Production nonce generation is already correct and was left untouched:

  • aa-runtime/src/ipc/handshake.rs::generate_nonce() uses getrandom (CSPRNG).
  • aa-runtime/src/ipc/server.rs:318 issues the live challenge via handshake::generate_nonce().

The 6 flagged flows all originated from hard-coded literal nonces in test/codec code added by this PR. Fixed at the 3 literal sources (which CodeQL expands into ~6 sink flows via clone + sign + verify):

File Was Now
aa-runtime/src/ipc/codec.rs:439 nonce: vec![3u8; 32] (+ matching assert) fresh handshake::generate_nonce(), asserted round-trip
aa-sdk-client/src/ipc.rs:257 let nonce = vec![42u8; 32] getrandom-filled 32-byte nonce
aa-sdk-client/tests/lifecycle_e2e.rs:41 let nonce = vec![9u8; 32] getrandom-filled 32-byte nonce

Each nonce is still signed and verified against the same (now random) value, so no assertion was weakened. aa-sdk-client gained a getrandom dev-dependency (0.2, already resolved in the lockfile, mirrors the runtime's production source).

Local validation (touched crates): aa-runtime + aa-sdk-client 367/367 pass; the runtime↔gateway handshake e2e (runtime_forwards_per_tool_deny_to_gateway) passes; cargo build, clippy -D warnings, and cargo fmt --check all clean.

CodeQL should report 0 new alerts on re-analysis.

— Claude Code

…ar CodeQL hard-coded-crypto)

CodeQL's "Hard-coded cryptographic value" (Critical) flagged the
`[0u8; NONCE_LEN]` buffer literal at handshake.rs:36 as a hard-coded
value flowing into a nonce: getrandom's in-place `&mut` fill is not
recognized as a sanitizer, so the zero-init literal stayed in the
taint path. Produce the 32 bytes directly from `rand::random` (a
ChaCha-based, OS-seeded thread CSPRNG that returns the value), so no
constant literal enters the nonce data-flow. Runtime randomness is
unchanged/strengthened; NONCE_LEN preserved. Drops the now-unused
direct getrandom dep (rand 0.8 was already a transitive dep via
async-nats, so the dep graph is unchanged; cargo deny stays clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Chisanan232

Copy link
Copy Markdown
Contributor Author

CodeQL "Hard-coded cryptographic value" (Critical) — real source fixed

The previous attempt randomized the test nonces, but the Critical alert's source is production code: aa-runtime/src/ipc/handshake.rs:36, the let mut nonce = [0u8; NONCE_LEN]; buffer literal inside generate_nonce().

CodeQL's taint model does not recognize getrandom's in-place &mut fill as a sanitizer, so the zero-init literal remained in the data-flow and was reported (×6 sink flows from this one source) as a hard-coded value used as a nonce. The nonce was random at runtime, but the constant literal sat in the taint path.

Fix (commit 1ef5dc50): produce the 32 bytes directly from a value-returning CSPRNG so no constant literal enters the nonce data-flow:

pub fn generate_nonce() -> [u8; NONCE_LEN] {
    rand::random::<[u8; NONCE_LEN]>()
}

rand::random draws from a ChaCha-based, OS-seeded thread CSPRNG and returns the value. Production randomness is unchanged/strengthened — NONCE_LEN is preserved and the source remains a CSPRNG. The now-unused direct getrandom dependency was dropped; rand 0.8 was already a transitive dependency (via async-nats → nkeys), so the dependency graph is unchanged and cargo deny check stays clean (advisories/bans/licenses/sources all ok).

Local validation: cargo nextest run -p aa-runtime → 317 passed (incl. nonce_is_32_bytes_and_varies and all handshake/server tests); cargo build -p aa-runtime ✓; cargo clippy -p aa-runtime --all-targets -- -D warnings clean; cargo fmt --check clean; cargo deny check ok. Pre-commit and pre-push (cargo doc) hooks passed.

I'm not asserting the CodeQL check is green — leaving the CI re-analysis on the new head to confirm.

— Claude Code

@Chisanan232
Chisanan232 merged commit c905a88 into master Jun 24, 2026
66 of 67 checks passed
@Chisanan232
Chisanan232 deleted the v0.0.1/AAASM-3569/sdk_runtime_ipc_auth branch June 24, 2026 01:46
Chisanan232 added a commit that referenced this pull request Jun 24, 2026
CodeQL CRITICAL "hard-coded value used as a nonce" at ipc.rs:369/387 — the
test helpers used `[0u8; 32]`/`vec![0u8; 32]` + getrandom in-place fill, whose
buffer literal CodeQL flags as flowing into the signed nonce (it doesn't model
getrandom's &mut fill as sanitizing). Switch to value-returning rand::random()
so no literal is in the data-flow. Same fix as the #1216 production handshake.
No behavior change. Refs AAASM-3666.
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