Skip to content

feat(studio): add optional remote host support over SSH - #8

Open
drgost1 wants to merge 1 commit into
qbcore-framework:mainfrom
drgost1:feat/remote-ssh-host
Open

drgost1 wants to merge 1 commit into
qbcore-framework:mainfrom
drgost1:feat/remote-ssh-host

Conversation

@drgost1

@drgost1 drgost1 commented Aug 31, 2026

Copy link
Copy Markdown

Summary

Adds an opt-in remote mode so a workspace can target an FXServer on another machine over SSH. When StudioConfig.remote is unset — the default — every existing code path runs unchanged.

Opening this as a concrete proposal rather than a request; entirely understood if the direction isn't one you want, and happy to close it or rework it.

The loopback policy is not relaxed

The obvious version of this feature — let RCON_HOST accept a public address — deserves rejection. FXServer RCON is Quake3-style UDP with the password in plaintext in every packet, so that would leak rcon_password on every command. networkPolicy.ts is correct as written and this PR does not touch it.

Instead the policy is satisfied honestly on both ends:

Hop Address Notes
Runtime bind on host 127.0.0.1 genuinely loopback there
Runtime → FXServer RCON 127.0.0.1 UDP never leaves the host
Studio → forwarded port 127.0.0.1 genuinely loopback here
Between the two SSH encryption, auth, host-key verification

assertLoopbackHost() passes on both sides truthfully and unmodified. The RCON password never touches the network at all.

Why the change is small

managedRuntime.ts already spawns the runtime with a clean contract — environment variables in, authenticated HTTP on a port out — with no local-only coupling. Remote mode reuses it exactly, swapping spawn() for ssh plus a port forward.

fivem-mcp-server is unchanged. It is already remote-capable: it binds from MCP_HOST/MCP_PORT, and httpServer.ts always prints the listening port to stderr. Remote mode parses that line in place of the IPC ready message, which isn't available over SSH (process.send is correctly guarded, so nothing breaks).

Implementation notes

  • No new dependencies. The system OpenSSH client is invoked directly, so DEPENDENCY_POLICY.md's review gate is unaffected, and ~/.ssh/config, agent, and key handling come for free. BatchMode=yes prevents interactive prompts from hanging the UI.
  • No secrets in argv. The launch script is delivered over SSH stdin (sh -s), so nothing is visible via ps to other users on the host.
  • rcon_password is never transmitted. The remote script reads it out of the same server.cfg the runtime is given, on the host.
  • No secrets in StudioConfig. That object reaches the renderer, so RemoteHostSettings holds only a host alias and paths.
  • Two SSH processes: one runs the runtime (MCP_PORT=0, host picks a free port), one forwards once that port is known. This avoids reserving a fixed remote port.
  • remoteSettingsOrNull() validates in the style of the surrounding code: absolute POSIX paths with no traversal, a conservative host-alias charset that cannot start with -, port range, and the runtime's own both-or-neither txAdmin pairing plus its server.cfg-inside-workspace rule.

Scope

This is stage 1 of the smallest useful split:

  • Stage 1 (this PR) — remote runtime + forward. Console, resource list, and lifecycle actions work remotely. Editing stays local-only.
  • Stage 2 — move workspace file operations from Electron main behind the runtime's HTTP boundary so remote editing works. The existing traversal guards, symlink defenses, atomic writes, and revision conflicts get ported, not reinvented. Worth doing on its own merits since it collapses two file-access paths into one.
  • Stage 3 — remote server lifecycle (hosts generally use a service manager rather than a txAdmin-owned local process).

No settings UI yet — deliberately kept out to keep this reviewable. Happy to add it if the approach looks right.

Verification

npm run typecheck and npm run check:static both pass.

Verified end-to-end against a live FXServer (Linux host, Node 24.20.0, Windows client): the bundled runtime.cjs deployed unmodified, initialize + tools/list + tools/call all succeed through the tunnel, list_resources returns real started/stopped state, and get_runtime_identity reports the host's workspace path.

Not run here: npm test, npm run dist, and npm run verify:package, since those need the full release toolchain on the pinned Node 24.20.0 / npm 11.19.0 — worth running before merge.

Adds an opt-in remote mode so a workspace can target an FXServer on another
machine. When StudioConfig.remote is unset (the default), every existing code
path runs unchanged.

The loopback policy is not relaxed; it is satisfied honestly on both ends. The
runtime binds 127.0.0.1 on the host and reaches RCON at 127.0.0.1, so the
Quake3-style UDP packets that carry rcon_password in plaintext never leave that
machine. The app connects to 127.0.0.1 on a forwarded port. SSH provides
encryption, authentication, and host-key verification in between.

networkPolicy.ts and fivem-mcp-server are unchanged. The runtime is already
remote-capable: it binds from MCP_HOST/MCP_PORT and always prints its listening
port to stderr, which remote mode parses in place of the IPC ready message that
is unavailable over SSH.

Notes:
- No new dependencies. The system OpenSSH client is invoked directly, which
  also inherits the user's ~/.ssh/config, agent, and key handling.
- The launch script is delivered over SSH stdin (sh -s), so no value appears in
  argv, where other users on the host could read it via ps.
- rcon_password is read out of server.cfg by the remote script itself, so it is
  never transmitted to the client.
- RemoteHostSettings holds no secrets, since StudioConfig reaches the renderer.

Verified against a live FXServer: initialize, tools/list, and tools/call all
succeed through the tunnel, and list_resources returns real started/stopped
state.
@drgost1
drgost1 force-pushed the feat/remote-ssh-host branch from 6048acb to afa0b6e Compare August 31, 2026 08:07
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