Agent

Agent forwards an SSH agent through rmote between POSIX endpoints. It needs an existing agent socket on the agent endpoint. rmote transfers its forwarding code automatically; the listener endpoint needs no installed rmote package or sshd. The program using the forwarded agent must be installed there.

Choose the endpoints

Call Agent.forward directly as an async context manager. Do not pass it to remote(...). Each endpoint is an open async Protocol; None or an omitted argument selects the current Python process. The synchronous Connection cannot be used as an endpoint for this context manager.

Call

Agent socket

New listener socket

Agent.forward(listener=remote)

Local

Remote

Agent.forward(agent=remote)

Remote

Local

Agent.forward(listener=host_a, agent=host_b)

Host B

Host A

path= selects an existing socket on the agent endpoint. If omitted, the Tool reads SSH_AUTH_SOCK on that endpoint. The context yields the path of a new socket on the listener endpoint. Set the consuming process’s SSH_AUTH_SOCK to this path. The context does not change process environments.

Use a local agent remotely

Run this example with a local agent containing a key and SSH_AUTH_SOCK set. It uses a local subprocess as the target so it runs without an SSH server. Replace Protocol.from_command(python=sys.executable) with an appropriate transport to reach another host.

import asyncio
import sys

from rmote.protocol import Protocol
from rmote.tools import Agent, Exec


async def main():
    async with await Protocol.from_command(python=sys.executable) as remote:
        async with Agent.forward(listener=remote) as socket_path:
            result = await remote(
                Exec.command, "ssh-add", "-L",
                env={"SSH_AUTH_SOCK": socket_path}, capture_output=True,
            )
            assert result.returncode == 0, result.stderr
            print(result.stdout.decode())


asyncio.run(main())

ssh-add runs on the listener endpoint and contacts the local agent through the yielded socket. Pass path="/path/to/local/agent.sock" to select a known socket instead of the local SSH_AUTH_SOCK.

Use a remote agent locally

This example reverses the direction. The subprocess inherits SSH_AUTH_SOCK for this local demonstration. With a remote transport, use the remote environment or pass path="/path/to/remote/agent.sock" explicitly.

import asyncio
import os
import sys

from rmote.protocol import Protocol
from rmote.tools import Agent


async def main():
    async with await Protocol.from_command(python=sys.executable) as remote:
        async with Agent.forward(agent=remote) as socket_path:
            process = await asyncio.create_subprocess_exec(
                "ssh-add", "-L",
                env=dict(os.environ, SSH_AUTH_SOCK=socket_path),
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            output, error = await process.communicate()
            assert process.returncode == 0, error
            print(output.decode())


asyncio.run(main())

Lifetime and errors

Keep the endpoint Protocols and the forwarding context open while consumers use the socket. Bytes pass through the coordinator process; two remote endpoints do not open a direct connection to each other. Exiting the context closes both sessions and their connections and removes the listener socket and its private temporary directory. Startup failure and cancellation also trigger cleanup. Forcefully terminating an endpoint can leave its temporary directory behind; its sockets can no longer forward requests.

Startup probes the source socket before creating the listener. An unset path or an inaccessible socket raises OSError; connection attempts have a five-second timeout. Unlike the context manager, the sshmux CLI handles an unavailable agent by warning and continuing without forwarding.

Each endpoint session permits 32 connections. A listener uses a mode 0600 socket inside a mode 0700 directory. The relay sends at most 64 KiB per chunk and preserves half-close, so a final response can follow request EOF.

This is a byte relay. It adds no OpenSSH session-bind@openssh.com binding for the rmote hop. Destination constraints and restrictions that depend on recognizing a forwarded connection cannot validate this hop. See the sshmux guide for CLI usage and this limitation.

Endpoint operations

Agent.forward manages these operations for you. For a custom coordinator, call them locally or through Protocol. Start a session on each endpoint and pass its session_id to every operation. Connection IDs belong only to their session; an unknown session or connection raises ConnectionError.

Operation

Result or effect

start(session_id=None)

Create a session and return its ID

listen(session_id)

Create the session’s listener and return its path

accept(session_id)

Stream accepted connection IDs; closing this stream releases the listener session

connect(session_id, path=None, connection_id=None)

Connect to an existing agent socket and return a connection ID

output(session_id, connection_id)

Stream incoming bytes; only one reader per connection

write(session_id, connection_id, data)

Write a chunk and wait for buffer drainage

end_input(session_id, connection_id)

Send EOF while allowing output to continue

close(session_id, connection_id)

Close one connection; safe to repeat

release(session_id)

Close and remove the session; safe to repeat

Supply unique IDs before resource creation if cancellation can prevent you from receiving a result. Wait for outstanding start, listen, and connect calls before releasing their sessions: cancelling local waiting does not cancel an ordinary remote RPC. Prefer Agent.forward for this coordination.

API reference

class rmote.tools.agent.Agent[source]

Bases: Tool

Forward agents between independently selected rmote endpoints.

Methods run on the remote event loop. A session ID is required for every operation; connection IDs from another session are rejected. Call release after all outstanding start calls have completed, including on cancellation.

CHUNK_SIZE: ClassVar[int] = 65536
MAX_CONNECTIONS: ClassVar[int] = 32
classmethod accept(session_id)[source]
Return type:

AsyncGenerator[Text, None]

async classmethod close(session_id, connection_id)[source]
Return type:

None

async classmethod connect(session_id, path=None, connection_id=None)[source]

Connect to an agent on this endpoint and return a connection ID.

With path=None, read SSH_AUTH_SOCK on this endpoint. An explicit connection ID lets a caller clean up after a cancelled response.

Return type:

Text

async classmethod end_input(session_id, connection_id)[source]
Return type:

None

static forward(*, listener=None, agent=None, path=None)[source]

Expose an agent through a private socket on the listener endpoint.

Each endpoint is an open Protocol, or None for this Python process. The returned path belongs to the listener endpoint. Resolve path (or SSH_AUTH_SOCK when omitted) on the agent endpoint. Examples:

async with Agent.forward(listener=remote) as remote_socket:
    ...  # local agent, remote listener
async with Agent.forward(agent=remote) as local_socket:
    ...  # remote agent, local listener
async with Agent.forward(listener=host_a, agent=host_b) as socket_on_a:
    ...  # agent on B, listener on A

Call this context manager directly, like FileSync.upload. It manages session-owned Tool operations on both endpoints and releases both sessions on exit. The relay does not add an OpenSSH session binding.

Return type:

AsyncGenerator[Text, None]

async classmethod listen(session_id)[source]

Create a private listener on this endpoint and return its path.

Return type:

Text

classmethod output(session_id, connection_id)[source]
Return type:

AsyncGenerator[bytes, None]

async classmethod release(session_id)[source]
Return type:

None

async classmethod start(session_id=None)[source]

Create a session on this endpoint and return its ID.

Return type:

Text

async classmethod write(session_id, connection_id, data)[source]
Return type:

None