Extract the SSH agent socket provider API to a separate PyPI package
Marco Ricci

Marco Ricci commited on 2026-08-30 10:25:08
Zeige 7 geänderte Dateien mit 202 Einfügungen und 0 Löschungen.


A new Python package `derivepassphrase-sshagentsocketprovider` contains
the API definition and types necessary to write SSH agent socket
providers, without depending on the rest of `derivepassphrase`.

Because this is so intricately tied to `derivepassphrase`, the new
package does not set up a separate repository, separate documentation or
a separate wishlist.  The tests directory is functionally empty, because
there is nothing to test, from a unit test/code coverage perspective.
The README merely contains a short usage example.
... ...
@@ -339,6 +339,7 @@ relative_files = true
339 339
 [tool.hatch.build.targets.sdist]
340 340
 exclude = [
341 341
     'docs/changelog.d/*.md',
342
+    'sshagentsocketprovider/',
342 343
 ]
343 344
 
344 345
 [tool.hatch.build.targets.wheel]
... ...
@@ -0,0 +1,13 @@
1
+zlib License
2
+
3
+Copyright 2026 Marco Ricci <software@the13thletter.info>
4
+
5
+This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
6
+
7
+Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
8
+
9
+ 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
10
+
11
+ 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
12
+
13
+ 3. This notice may not be removed or altered from any source distribution.
... ...
@@ -0,0 +1,62 @@
1
+# derivepassphrase-sshagentsocketprovider
2
+
3
+[![PyPI - Version](https://img.shields.io/pypi/v/derivepassphrase-sshagentsocketprovider.svg)](https://pypi.org/project/derivepassphrase-sshagentsocketprovider)
4
+[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/derivepassphrase-sshagentsocketprovider.svg)](https://pypi.org/project/derivepassphrase-sshagentsocketprovider)
5
+
6
+An interface for SSH agent socket providers, which expose an abstract communication channel for an SSH agent to `derivepassphrase`.
7
+
8
+This package contains only the interface definition and the types involved.
9
+
10
+-----
11
+
12
+## Installation
13
+
14
+`derivepassphrase-sshagentsocketprovider` is a pure Python package, and may be easily installed with any `pip`-compatible Python package manager such as `pip`, `pipx`, or `uv`.
15
+(`pip` is distributed with Python 3 by default.)
16
+
17
+`derivepassphrase-sshagentsocketprovider` requires Python 3.9 or higher, as well as the [typing-extensions package][TYPING_EXTENSIONS].
18
+
19
+```console
20
+pip install derivepassphrase-sshagentsocketprovider
21
+```
22
+
23
+[TYPING_EXTENSIONS]: https://pypi.org/project/typing-extensions/
24
+
25
+## Registering an SSH agent socket provider
26
+
27
+The `SSHAgentSocket` represents the abstract communication channel to an SSH agent.
28
+Ensure that the abstract channel behaves like a socket with respect to the `sendall` and `recv` operations.
29
+The abstract channel must also be a context manager, which closes itself upon leaving the context, causing further `sendall` and `recv` operations to raise an error.
30
+(By convention, this is `OSError`, with `errno.EBADF`.)
31
+
32
+The `SSHAgentSocketProvider` is a callable that returns an `SSHAgentSocket` when called without arguments.
33
+
34
+To then actually register an SSH agent socket provider, build an `SSHAgentSocketProviderEntry` struct as follows:
35
+
36
+~~~ python
37
+def ssh_agent_over_stdin_stdout_provider() -> SSHAgentSocket: ...
38
+
39
+
40
+stdin_stdout_entry_point = SSHAgentSocketProviderEntry(
41
+    provider=ssh_agent_over_stdin_stdout_provider,
42
+    key="stdin_stdout",
43
+    aliases=("stdin", "stdout"),
44
+)
45
+~~~
46
+
47
+Then add an appropriate entry point definition in your `pyproject.toml`, using the entry point group name `derivepassphrase.ssh_agent_socket_providers`:
48
+
49
+~~~ toml
50
+[project.entry."derivepassphrase.ssh_agent_socket_providers"]
51
+first_provider = "mymodule: stdin_stdout_entry_point"
52
+~~~
53
+
54
+`derivepassphrase` will then pick up `stdin_stdout` as a new SSH agent socket provider, with aliases `stdin` and `stdout`.[^entry_point_name]
55
+
56
+  [^entry_point_name]:
57
+    Only the fields in `SSHAgentSocketProviderEntry` matter, not what names are used to declare the entry point.
58
+    By convention, however, you would choose `stdin_stdout` as the entry point name, not `first_provider`.
59
+
60
+## License
61
+
62
+Like `derivepassphrase`, `derivepassphrase-sshagentsocketprovider` is distributed under the terms of the [zlib/libpng license](https://spdx.org/licenses/Zlib.html).
... ...
@@ -0,0 +1,38 @@
1
+[build-system]
2
+requires = ["hatchling"]
3
+build-backend = "hatchling.build"
4
+
5
+[project]
6
+name = "derivepassphrase-sshagentsocketprovider"
7
+description = 'An interface for SSH agent socket providers, which expose an abstract communication channel for an SSH agent to derivepassphrase.'
8
+readme = "README.md"
9
+version = "1.0a1"
10
+requires-python = ">= 3.9"
11
+license = "Zlib"
12
+keywords = []
13
+authors = [
14
+    { name = "Marco Ricci", email = "software@the13thletter.info" },
15
+]
16
+classifiers = [
17
+    "Development Status :: 4 - Beta",
18
+    "Intended Audience :: Developers",
19
+    "Operating System :: OS Independent",
20
+    "Programming Language :: Python :: 3",
21
+    "Programming Language :: Python :: 3.9",
22
+    "Programming Language :: Python :: 3.10",
23
+    "Programming Language :: Python :: 3.11",
24
+    "Programming Language :: Python :: 3.12",
25
+    "Programming Language :: Python :: 3.13",
26
+    "Programming Language :: Python :: 3.14",
27
+    "Programming Language :: Python :: 3.15",
28
+    "Programming Language :: Python :: Implementation :: CPython",
29
+    "Programming Language :: Python :: Implementation :: PyPy",
30
+    "Topic :: Software Development :: Libraries",
31
+    "Typing :: Typed",
32
+]
33
+dependencies = []
34
+
35
+[project.urls]
36
+Documentation = "https://the13thletter.info/derivepassphrase/"
37
+Issues = "https://the13thletter.info/derivepassphrase/latest/wishlist/"
38
+Source = "https://git.schokokeks.org/derivepassphrase.git"
... ...
@@ -0,0 +1,85 @@
1
+# SPDX-FileCopyrightText: 2026 Marco Ricci <software@the13thletter.info>
2
+#
3
+# SPDX-License-Identifier: Zlib
4
+
5
+"""Definitions for `derivepassphrase`'s SSH agent socket providers."""
6
+
7
+from __future__ import annotations
8
+
9
+from typing import TYPE_CHECKING, Protocol
10
+
11
+from typing_extensions import NamedTuple, runtime_checkable
12
+
13
+if TYPE_CHECKING:
14
+    from collections.abc import Callable
15
+
16
+    from typing_extensions import Any, Buffer, TypeAlias
17
+
18
+# Semantic versioning.
19
+__version__ = "1.0a1"
20
+
21
+# For later major versions, we would choose a different entry point
22
+# group name.
23
+ENTRY_POINT_GROUP_NAME = "derivepassphrase.ssh_agent_socket_providers"
24
+
25
+
26
+@runtime_checkable
27
+class SSHAgentSocket(Protocol):
28
+    """An abstract networking socket connected to an SSH agent.
29
+
30
+    The abstract socket supports the [`sendall`][socket.socket.sendall]
31
+    and a [`recv`][socket.socket.recv] operation, with the same
32
+    signatures and semantics as for "real" sockets.  The abstract socket
33
+    also supports use as a context manager, for automatically closing
34
+    the socket upon exiting the context.
35
+
36
+    """
37
+
38
+    def __enter__(self) -> Any:  # noqa: ANN401
39
+        """Returns self."""
40
+
41
+    # mypy/typeshed has a *very* lax annotation of
42
+    # socket.socket.__exit__, which we need to be compatible with.
43
+    # *sigh*
44
+    def __exit__(self, *args: object) -> bool | None:
45
+        """Closes the socket."""
46
+
47
+    def sendall(self, data: Buffer, flags: int = 0, /) -> None:
48
+        """Like [socket.socket.sendall][]."""
49
+
50
+    def recv(self, bufsize: int, flags: int = 0, /) -> bytes:
51
+        """Like [socket.socket.recv][]."""
52
+
53
+
54
+SSHAgentSocketProvider: TypeAlias = "Callable[[], SSHAgentSocket]"
55
+"""A callable that provides an SSH agent socket."""
56
+
57
+
58
+# For later major versions, we would define a new type.  For minor
59
+# versions, we may add new fields.
60
+class SSHAgentSocketProviderEntry(NamedTuple):
61
+    """Registry information for the table of SSH agent socket providers.
62
+
63
+    Third-party developers can register new socket providers for
64
+    auto-discovery by setting up an entry point named
65
+    [`derivepassphrase.ssh_agent_socket_providers`][ENTRY_POINT_GROUP_NAME],
66
+    referencing an instance of this class.  `derivepassphrase` will
67
+    then add appropriate entries to the registry.
68
+
69
+    Attributes:
70
+        provider: The callable that provides the socket.
71
+        key: The table key which this entry is registered as.
72
+        aliases: Other keys that shall point to this entry's key.
73
+
74
+    Note:
75
+        The socket provider registry table uses the key as the key, and
76
+        the provider as the value. It does not store this info object
77
+        directly.
78
+
79
+    """
80
+
81
+    provider: SSHAgentSocketProvider
82
+    """"""
83
+    key: str
84
+    """"""
85
+    aliases: tuple[str, ...]
... ...
@@ -0,0 +1,3 @@
1
+# SPDX-FileCopyrightText: 2026 Marco Ricci <software@the13thletter.info>
2
+#
3
+# SPDX-License-Identifier: Zlib
0 4