Do you already have an In Circuit Emulation ('ICE') feature implemented in HW? There are different ways to do it (Vendors often roll their own, esp when it comes to FPGAs). Once you have that, you just feed opcodes to your target via JTAG (what a proxy like OpenOCD or gdbproxy does when connecting via gdb).
There's also a virtualized debug TAP (test access port via ICE) found in my in-house setup (https://section5.ch/index.php/2019/10/24/risc-v-in-the-loop/
) which you can spin in a virtual machine (Docker container) and access/test the co-simulated HW via Python scripts. However this is very lean & mean, doesn't follow the rather complex official RISC-V spec and should only serve as example on how to do basic ICE.