agent-brain-uds 10.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.1
2
+ Name: agent-brain-uds
3
+ Version: 10.1.2
4
+ Summary: Agent Brain UDS - Unix-domain-socket transport for Agent Brain (socket path resolution, permission validation, httpx UDS client factory)
5
+ Home-page: https://github.com/SpillwaveSolutions/agent-brain
6
+ License: MIT
7
+ Keywords: agent-brain,uds,unix-domain-socket,transport,httpx,ipc
8
+ Author: Spillwave Solutions
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: POSIX
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Dist: httpx (>=0.28.0,<0.29.0)
20
+ Project-URL: Documentation, https://github.com/SpillwaveSolutions/agent-brain/wiki
21
+ Project-URL: Repository, https://github.com/SpillwaveSolutions/agent-brain
22
+ Description-Content-Type: text/markdown
23
+
24
+ # agent-brain-uds
25
+
26
+ Unix-domain-socket transport for [Agent Brain](https://github.com/SpillwaveSolutions/agent-brain).
27
+
28
+ **Client-side only.** This package provides:
29
+
30
+ - Socket path resolution (`resolve_socket_path`) consistent with `agent-brain-server`'s state-dir layout.
31
+ - Permission validation (`validate_socket`) — owner-UID match, no group/world bits, no-symlink check.
32
+ - `httpx`-compatible client factory (`make_client` / `make_async_client`) speaking HTTP/1.1 over UDS.
33
+
34
+ The corresponding server-side bind (`agent_brain_server.api.uds_bind`) lives in `agent-brain-server` to keep the dep direction acyclic.
35
+
36
+ ## Status
37
+
38
+ Phase 0 scaffold (10.0.7) — public surface lands in Phase 1.
39
+ See [`docs/plans/2026-05-28-mcp-uds-transport-design.md`](../docs/plans/2026-05-28-mcp-uds-transport-design.md).
40
+
41
+ ## License
42
+
43
+ MIT
44
+
@@ -0,0 +1,20 @@
1
+ # agent-brain-uds
2
+
3
+ Unix-domain-socket transport for [Agent Brain](https://github.com/SpillwaveSolutions/agent-brain).
4
+
5
+ **Client-side only.** This package provides:
6
+
7
+ - Socket path resolution (`resolve_socket_path`) consistent with `agent-brain-server`'s state-dir layout.
8
+ - Permission validation (`validate_socket`) — owner-UID match, no group/world bits, no-symlink check.
9
+ - `httpx`-compatible client factory (`make_client` / `make_async_client`) speaking HTTP/1.1 over UDS.
10
+
11
+ The corresponding server-side bind (`agent_brain_server.api.uds_bind`) lives in `agent-brain-server` to keep the dep direction acyclic.
12
+
13
+ ## Status
14
+
15
+ Phase 0 scaffold (10.0.7) — public surface lands in Phase 1.
16
+ See [`docs/plans/2026-05-28-mcp-uds-transport-design.md`](../docs/plans/2026-05-28-mcp-uds-transport-design.md).
17
+
18
+ ## License
19
+
20
+ MIT
@@ -0,0 +1,62 @@
1
+ """Agent Brain UDS — Unix-domain-socket transport for Agent Brain.
2
+
3
+ Client-side only. See docs/plans/2026-05-28-mcp-uds-transport-design.md §4.1.
4
+
5
+ Public surface:
6
+
7
+ - :func:`resolve_socket_path` — Resolve the canonical UDS socket path.
8
+ - :func:`validate_socket` — Validate socket file ownership and permissions.
9
+ - :func:`make_client` / :func:`make_async_client` — ``httpx`` clients
10
+ pre-configured for UDS transport.
11
+ - :class:`AgentBrainUdsError` and subclasses — Exception hierarchy.
12
+
13
+ The server-side bind helper lives inside ``agent_brain_server`` to keep
14
+ the dependency direction acyclic (server has no upward deps).
15
+ """
16
+
17
+ from .client import (
18
+ BASE_URL,
19
+ DEFAULT_TIMEOUT,
20
+ make_async_client,
21
+ make_client,
22
+ )
23
+ from .errors import (
24
+ AgentBrainUdsError,
25
+ SocketNotFoundError,
26
+ SocketPathTooLongError,
27
+ SocketPermissionError,
28
+ SocketStaleError,
29
+ )
30
+ from .paths import (
31
+ MAX_SOCKET_PATH_BYTES,
32
+ POINTER_FILE_NAME,
33
+ SOCKET_FILE_NAME,
34
+ STATE_DIR_NAME,
35
+ resolve_socket_path,
36
+ resolve_state_dir,
37
+ write_pointer_file,
38
+ )
39
+ from .permissions import validate_socket
40
+
41
+ __version__ = "10.1.2"
42
+
43
+ __all__ = [
44
+ "BASE_URL",
45
+ "DEFAULT_TIMEOUT",
46
+ "MAX_SOCKET_PATH_BYTES",
47
+ "POINTER_FILE_NAME",
48
+ "SOCKET_FILE_NAME",
49
+ "STATE_DIR_NAME",
50
+ "AgentBrainUdsError",
51
+ "SocketNotFoundError",
52
+ "SocketPathTooLongError",
53
+ "SocketPermissionError",
54
+ "SocketStaleError",
55
+ "__version__",
56
+ "make_async_client",
57
+ "make_client",
58
+ "resolve_socket_path",
59
+ "resolve_state_dir",
60
+ "validate_socket",
61
+ "write_pointer_file",
62
+ ]
@@ -0,0 +1,84 @@
1
+ """``httpx`` client factory for UDS transport.
2
+
3
+ Returns a configured ``httpx.Client`` / ``httpx.AsyncClient`` that speaks
4
+ HTTP/1.1 over the project's UDS socket. The ``base_url`` is a placeholder
5
+ (``http://agent-brain``) because ``httpx`` requires a URL even though UDS
6
+ ignores the host component — using a fixed sentinel makes log messages
7
+ self-documenting.
8
+
9
+ The client validates socket permissions on construction so a stale or
10
+ hijacked socket fails loud immediately, not on the first request.
11
+
12
+ See docs/plans/2026-05-28-mcp-uds-transport-design.md §6.3.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ from pathlib import Path
19
+
20
+ import httpx
21
+
22
+ from .paths import resolve_socket_path
23
+ from .permissions import validate_socket
24
+
25
+ #: Sentinel base URL that ends up in logs / error messages.
26
+ BASE_URL = "http://agent-brain"
27
+
28
+ #: Default request timeout (seconds). Overridable per call site.
29
+ DEFAULT_TIMEOUT = 30.0
30
+
31
+
32
+ def _resolve_path(
33
+ state_dir: Path | None,
34
+ socket_path: Path | None,
35
+ ) -> Path:
36
+ """Decide which socket path to use.
37
+
38
+ Precedence: explicit ``socket_path`` argument → ``AGENT_BRAIN_UDS_PATH``
39
+ env var → ``resolve_socket_path(state_dir)``.
40
+ """
41
+ if socket_path is not None:
42
+ return socket_path
43
+ env_path = os.environ.get("AGENT_BRAIN_UDS_PATH")
44
+ if env_path:
45
+ return Path(env_path).expanduser()
46
+ return resolve_socket_path(state_dir)
47
+
48
+
49
+ def make_client(
50
+ *,
51
+ state_dir: Path | None = None,
52
+ socket_path: Path | None = None,
53
+ timeout: float = DEFAULT_TIMEOUT,
54
+ ) -> httpx.Client:
55
+ """Return a synchronous ``httpx.Client`` configured for UDS.
56
+
57
+ Args:
58
+ state_dir: Override the state-directory resolver. Defaults to
59
+ ``AGENT_BRAIN_STATE_DIR`` env var or CWD lookup.
60
+ socket_path: Override the entire path-resolution chain.
61
+ timeout: HTTP request timeout in seconds.
62
+
63
+ Raises:
64
+ SocketNotFoundError: when the resolved socket does not exist.
65
+ SocketPermissionError: when the socket fails the permission checks
66
+ in :mod:`agent_brain_uds.permissions`.
67
+ """
68
+ path = _resolve_path(state_dir, socket_path)
69
+ validate_socket(path)
70
+ transport = httpx.HTTPTransport(uds=str(path))
71
+ return httpx.Client(transport=transport, base_url=BASE_URL, timeout=timeout)
72
+
73
+
74
+ def make_async_client(
75
+ *,
76
+ state_dir: Path | None = None,
77
+ socket_path: Path | None = None,
78
+ timeout: float = DEFAULT_TIMEOUT,
79
+ ) -> httpx.AsyncClient:
80
+ """Async counterpart of :func:`make_client`."""
81
+ path = _resolve_path(state_dir, socket_path)
82
+ validate_socket(path)
83
+ transport = httpx.AsyncHTTPTransport(uds=str(path))
84
+ return httpx.AsyncClient(transport=transport, base_url=BASE_URL, timeout=timeout)
@@ -0,0 +1,74 @@
1
+ """Exception hierarchy for agent-brain-uds.
2
+
3
+ Each error carries the resolved socket path and a remediation hint so the
4
+ caller (CLI, MCP server, or third-party) can surface an actionable message
5
+ to the user without re-implementing the diagnostics.
6
+
7
+ See docs/plans/2026-05-28-mcp-uds-transport-design.md §6.7.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+
14
+
15
+ class AgentBrainUdsError(Exception):
16
+ """Base class for all agent-brain-uds errors.
17
+
18
+ Subclasses carry a ``socket_path`` attribute (when known) and a
19
+ ``remediation`` hint that callers can show to the user.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ message: str,
25
+ *,
26
+ socket_path: Path | None = None,
27
+ remediation: str | None = None,
28
+ ) -> None:
29
+ super().__init__(message)
30
+ self.socket_path = socket_path
31
+ self.remediation = remediation
32
+
33
+ def __str__(self) -> str:
34
+ parts = [super().__str__()]
35
+ if self.socket_path is not None:
36
+ parts.append(f"(socket: {self.socket_path})")
37
+ if self.remediation:
38
+ parts.append(f"\n → {self.remediation}")
39
+ return " ".join(parts)
40
+
41
+
42
+ class SocketNotFoundError(AgentBrainUdsError):
43
+ """No socket file at the resolved path.
44
+
45
+ Most common cause: the server has not been started with ``--uds``.
46
+ """
47
+
48
+
49
+ class SocketStaleError(AgentBrainUdsError):
50
+ """Socket file exists but no listener is bound.
51
+
52
+ Typically left over from a crashed server process. The server's
53
+ own bind helper unlinks stale sockets on startup, so this should
54
+ be rare in practice.
55
+ """
56
+
57
+
58
+ class SocketPermissionError(AgentBrainUdsError):
59
+ """Socket file or its parent directory has unsafe permissions.
60
+
61
+ Raised when the socket file is owned by a different UID, has
62
+ group/world readable bits set, is a symlink, or its parent
63
+ directory is not mode 0700.
64
+ """
65
+
66
+
67
+ class SocketPathTooLongError(AgentBrainUdsError):
68
+ """Resolved socket path exceeds the OS sockaddr_un limit.
69
+
70
+ UDS paths are limited to ~104 bytes on macOS/BSD and ~108 on Linux.
71
+ The resolver falls back to ``/tmp/agent-brain-<sha8>.sock`` and writes
72
+ a pointer file; this error is raised only if even the fallback
73
+ cannot be used.
74
+ """
@@ -0,0 +1,200 @@
1
+ """Socket path resolution for agent-brain-uds.
2
+
3
+ Mirrors the state-directory lookup in ``agent_brain_server.runtime`` /
4
+ ``agent_brain_server.storage_paths`` so client and server agree on where
5
+ the socket lives. Handles the platform sockaddr_un length limit by
6
+ falling back to a short ``/tmp`` path and writing a pointer file inside
7
+ the state directory.
8
+
9
+ See docs/plans/2026-05-28-mcp-uds-transport-design.md §6.1.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import os
16
+ import stat
17
+ from pathlib import Path
18
+
19
+ from .errors import SocketPathTooLongError, SocketPermissionError
20
+
21
+ #: Default state-directory name. Mirrors ``STATE_DIR_NAME`` in the server.
22
+ STATE_DIR_NAME = ".agent-brain"
23
+
24
+ #: Default socket file name inside the state directory.
25
+ SOCKET_FILE_NAME = "agent-brain.sock"
26
+
27
+ #: Pointer-file name written alongside the canonical socket location when
28
+ #: the canonical path exceeds the platform limit. Contains the real
29
+ #: (short) socket path, one line, UTF-8.
30
+ POINTER_FILE_NAME = "agent-brain.sock.path"
31
+
32
+ #: Conservative sockaddr_un limit. macOS/BSD cap is 104 bytes; Linux is 108.
33
+ #: We use the smaller value so paths work on every supported platform.
34
+ MAX_SOCKET_PATH_BYTES = 104
35
+
36
+
37
+ def _short_fallback_path(state_dir: Path) -> Path:
38
+ """Return a short ``/tmp`` socket path derived from the state-dir hash.
39
+
40
+ The hash makes the fallback deterministic per project, so concurrent
41
+ instances in different projects do not collide.
42
+ """
43
+ digest = hashlib.sha256(str(state_dir.resolve()).encode("utf-8")).hexdigest()[:8]
44
+ return Path("/tmp") / f"agent-brain-{digest}.sock"
45
+
46
+
47
+ def resolve_state_dir(state_dir: Path | None = None) -> Path:
48
+ """Resolve the state directory using the same precedence as the server.
49
+
50
+ Order:
51
+
52
+ 1. Explicit ``state_dir`` argument.
53
+ 2. ``AGENT_BRAIN_STATE_DIR`` environment variable.
54
+ 3. ``<cwd>/.agent-brain/`` if it exists.
55
+ 4. Walk up from ``cwd`` looking for ``.agent-brain/``.
56
+ 5. Fall back to ``<cwd>/.agent-brain/`` (does not need to exist —
57
+ the server creates it).
58
+
59
+ Returns:
60
+ The resolved state-directory path (not guaranteed to exist).
61
+ """
62
+ if state_dir is not None:
63
+ return state_dir.resolve()
64
+
65
+ env_dir = os.environ.get("AGENT_BRAIN_STATE_DIR")
66
+ if env_dir:
67
+ return Path(env_dir).resolve()
68
+
69
+ cwd = Path.cwd().resolve()
70
+ candidate = cwd / STATE_DIR_NAME
71
+ if candidate.is_dir():
72
+ return candidate
73
+
74
+ # Walk upward looking for an existing .agent-brain/ directory.
75
+ for parent in cwd.parents:
76
+ candidate = parent / STATE_DIR_NAME
77
+ if candidate.is_dir():
78
+ return candidate
79
+
80
+ # Default: a (possibly non-existent) .agent-brain/ in the current dir.
81
+ return cwd / STATE_DIR_NAME
82
+
83
+
84
+ def resolve_socket_path(state_dir: Path | None = None) -> Path:
85
+ """Resolve the UDS socket path for the given state directory.
86
+
87
+ Reads a pointer file first if present (long-path fallback), then
88
+ falls back to ``<state_dir>/agent-brain.sock``. If even the canonical
89
+ path is too long for the platform, returns the short ``/tmp`` fallback
90
+ (the server is responsible for writing the pointer file when it binds).
91
+
92
+ Raises:
93
+ SocketPathTooLongError: when even the ``/tmp`` fallback exceeds
94
+ the platform limit (essentially impossible, but guarded for
95
+ completeness).
96
+ """
97
+ resolved_state_dir = resolve_state_dir(state_dir)
98
+
99
+ # If a previous server run dropped a pointer file, honor it — but
100
+ # validate it first (plan §8). The pointer file is a privilege
101
+ # boundary: whoever writes it controls every client's destination.
102
+ pointer = resolved_state_dir / POINTER_FILE_NAME
103
+ target = _read_pointer_file(pointer)
104
+ if target is not None:
105
+ if len(str(target).encode("utf-8")) >= MAX_SOCKET_PATH_BYTES:
106
+ raise SocketPathTooLongError(
107
+ "Pointer-file target exceeds platform socket-path limit.",
108
+ socket_path=target,
109
+ remediation=(
110
+ "Delete the pointer file and re-bind the server with "
111
+ "AGENT_BRAIN_UDS_PATH set to a path under 104 bytes."
112
+ ),
113
+ )
114
+ return target
115
+
116
+ canonical = resolved_state_dir / SOCKET_FILE_NAME
117
+ if len(str(canonical).encode("utf-8")) < MAX_SOCKET_PATH_BYTES:
118
+ return canonical
119
+
120
+ # Canonical path is too long; use the short fallback.
121
+ fallback = _short_fallback_path(resolved_state_dir)
122
+ if len(str(fallback).encode("utf-8")) >= MAX_SOCKET_PATH_BYTES:
123
+ raise SocketPathTooLongError(
124
+ "Both canonical and /tmp fallback paths exceed platform limit.",
125
+ socket_path=fallback,
126
+ remediation=("Set AGENT_BRAIN_UDS_PATH to a path shorter than 104 bytes."),
127
+ )
128
+ return fallback
129
+
130
+
131
+ def _read_pointer_file(pointer: Path) -> Path | None:
132
+ """Return the validated socket path from ``pointer`` or ``None``.
133
+
134
+ Defenses (plan §8 — Phase 5):
135
+
136
+ * ``os.lstat`` instead of ``Path.is_file`` so a symlink at the
137
+ pointer path is detected, not silently followed.
138
+ * Pointer must be a regular file; symlinks / sockets / fifos / dirs
139
+ are rejected outright.
140
+ * Pointer contents must be an absolute path with no embedded null
141
+ bytes.
142
+
143
+ ``validate_socket(...)`` still runs on the returned path before any
144
+ connection, so this is defense-in-depth — the goal is to fail fast
145
+ on obviously attacker-controlled inputs rather than to substitute
146
+ for the socket-level checks.
147
+ """
148
+ try:
149
+ pst = os.lstat(pointer)
150
+ except FileNotFoundError:
151
+ return None
152
+
153
+ if stat.S_ISLNK(pst.st_mode):
154
+ raise SocketPermissionError(
155
+ "Refusing to follow symlinked pointer file.",
156
+ socket_path=pointer,
157
+ remediation=(
158
+ f"Delete {pointer} (it is a symlink) and re-bind the "
159
+ "server, which will write a real pointer file."
160
+ ),
161
+ )
162
+ if not stat.S_ISREG(pst.st_mode):
163
+ raise SocketPermissionError(
164
+ "Pointer file is not a regular file.",
165
+ socket_path=pointer,
166
+ remediation=f"Delete {pointer} and re-bind the server.",
167
+ )
168
+
169
+ raw = pointer.read_bytes()
170
+ if b"\x00" in raw:
171
+ raise SocketPermissionError(
172
+ "Pointer file contains an embedded null byte; refusing to parse.",
173
+ socket_path=pointer,
174
+ remediation=f"Delete {pointer} and re-bind the server.",
175
+ )
176
+
177
+ target_str = raw.decode("utf-8", errors="strict").strip()
178
+ target = Path(target_str)
179
+ if not target.is_absolute():
180
+ raise SocketPermissionError(
181
+ f"Pointer-file target {target_str!r} is not an absolute path.",
182
+ socket_path=target,
183
+ remediation=(
184
+ f"Delete {pointer} and re-bind the server (or set "
185
+ "AGENT_BRAIN_UDS_PATH to an absolute path under 104 bytes)."
186
+ ),
187
+ )
188
+ return target
189
+
190
+
191
+ def write_pointer_file(state_dir: Path, real_socket_path: Path) -> Path:
192
+ """Write the pointer file used by long-path fallback.
193
+
194
+ Called by the server when binding to a ``/tmp`` fallback socket so
195
+ later clients can discover the real socket without recomputing.
196
+ """
197
+ state_dir.mkdir(parents=True, exist_ok=True)
198
+ pointer = state_dir / POINTER_FILE_NAME
199
+ pointer.write_text(str(real_socket_path))
200
+ return pointer
@@ -0,0 +1,110 @@
1
+ """Socket permission validation for agent-brain-uds.
2
+
3
+ Filesystem permissions are the auth model for local UDS: a socket file
4
+ that is mode 0600 owned by the current user is reachable only by that
5
+ user. We enforce that explicitly on connect so a symlink-hijack or a
6
+ world-readable socket fails loud instead of leaking traffic.
7
+
8
+ Adversarial test coverage lands in Phase 5 (plan §13). Phase 1 ships
9
+ the happy-path and basic rejection logic that those tests build on.
10
+
11
+ See docs/plans/2026-05-28-mcp-uds-transport-design.md §6.5 / §8.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import stat
18
+ from pathlib import Path
19
+
20
+ from .errors import SocketNotFoundError, SocketPermissionError
21
+
22
+ #: Mode bits we refuse to accept on the socket file (group + world rwx).
23
+ FORBIDDEN_SOCKET_BITS = stat.S_IRWXG | stat.S_IRWXO # 0o077
24
+
25
+ #: Mode we require on the parent directory of the socket file.
26
+ REQUIRED_PARENT_DIR_MODE = 0o700
27
+
28
+
29
+ def validate_socket(path: Path) -> None:
30
+ """Validate that a UDS socket file is safe for the current user to connect.
31
+
32
+ Checks (in order):
33
+
34
+ 1. The path is not a symlink (``os.lstat``).
35
+ 2. The path exists.
36
+ 3. The path is a socket file (``S_ISSOCK``).
37
+ 4. The path is owned by the current UID.
38
+ 5. The path has no group or world permission bits set.
39
+ 6. The parent directory is mode ``0700``.
40
+
41
+ Raises:
42
+ SocketNotFoundError: when the path does not exist.
43
+ SocketPermissionError: when any safety check fails.
44
+ """
45
+ # Use lstat to detect symlinks without following them. A symlink at
46
+ # the socket path is a classic privilege-escalation hook.
47
+ try:
48
+ st = os.lstat(path)
49
+ except FileNotFoundError as exc:
50
+ raise SocketNotFoundError(
51
+ f"No socket file at {path}.",
52
+ socket_path=path,
53
+ remediation=(
54
+ "Start the server with `agent-brain start --uds`, or set "
55
+ "AGENT_BRAIN_UDS_PATH to point at an existing socket."
56
+ ),
57
+ ) from exc
58
+
59
+ if stat.S_ISLNK(st.st_mode):
60
+ raise SocketPermissionError(
61
+ "Refusing to connect: socket path is a symlink.",
62
+ socket_path=path,
63
+ remediation=(
64
+ "Inspect the symlink target; if expected, delete the symlink "
65
+ "and re-bind the server, which will create a real socket file."
66
+ ),
67
+ )
68
+
69
+ if not stat.S_ISSOCK(st.st_mode):
70
+ raise SocketPermissionError(
71
+ "Refusing to connect: path exists but is not a socket file.",
72
+ socket_path=path,
73
+ remediation=(
74
+ "Delete or move the path and re-bind the server to recreate "
75
+ "the socket cleanly."
76
+ ),
77
+ )
78
+
79
+ current_uid = os.getuid()
80
+ if st.st_uid != current_uid:
81
+ raise SocketPermissionError(
82
+ f"Refusing to connect: socket owned by uid {st.st_uid}, "
83
+ f"current uid is {current_uid}.",
84
+ socket_path=path,
85
+ remediation=(
86
+ "Confirm you started the server as the current user, or "
87
+ "use sudo -u to run as the socket owner."
88
+ ),
89
+ )
90
+
91
+ if st.st_mode & FORBIDDEN_SOCKET_BITS:
92
+ raise SocketPermissionError(
93
+ f"Refusing to connect: socket mode {stat.S_IMODE(st.st_mode):#o} "
94
+ "includes group or world bits.",
95
+ socket_path=path,
96
+ remediation=(
97
+ "Re-bind the server (the bind helper chmod 0600s the socket); "
98
+ "or chmod 600 the existing socket if the server is trustworthy."
99
+ ),
100
+ )
101
+
102
+ parent_st = os.lstat(path.parent)
103
+ parent_mode = stat.S_IMODE(parent_st.st_mode)
104
+ if parent_mode != REQUIRED_PARENT_DIR_MODE:
105
+ raise SocketPermissionError(
106
+ f"Refusing to connect: parent directory mode is {parent_mode:#o}, "
107
+ f"expected {REQUIRED_PARENT_DIR_MODE:#o}.",
108
+ socket_path=path,
109
+ remediation=(f"chmod 700 {path.parent} and re-bind the server."),
110
+ )
@@ -0,0 +1,61 @@
1
+ [tool.poetry]
2
+ name = "agent-brain-uds"
3
+ version = "10.1.2"
4
+ description = "Agent Brain UDS - Unix-domain-socket transport for Agent Brain (socket path resolution, permission validation, httpx UDS client factory)"
5
+ authors = ["Spillwave Solutions"]
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ homepage = "https://github.com/SpillwaveSolutions/agent-brain"
9
+ repository = "https://github.com/SpillwaveSolutions/agent-brain"
10
+ documentation = "https://github.com/SpillwaveSolutions/agent-brain/wiki"
11
+ keywords = ["agent-brain", "uds", "unix-domain-socket", "transport", "httpx", "ipc"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Operating System :: POSIX",
21
+ ]
22
+ packages = [{include = "agent_brain_uds"}]
23
+
24
+ [tool.poetry.dependencies]
25
+ python = "^3.10"
26
+ httpx = "^0.28.0"
27
+ # agent-brain-rag dependency added in Phase 1 when models are imported.
28
+
29
+ [tool.poetry.group.dev.dependencies]
30
+ pytest = "^8.3.0"
31
+ pytest-asyncio = "^0.24.0"
32
+ pytest-cov = "^6.0.0"
33
+ black = "^24.10.0"
34
+ ruff = "^0.8.0"
35
+ mypy = "^1.13.0"
36
+ # Roundtrip test spawns a minimal ASGI app bound to a UDS socket via uvicorn.
37
+ uvicorn = "^0.32.0"
38
+ starlette = "^0.41.0"
39
+
40
+ [build-system]
41
+ requires = ["poetry-core"]
42
+ build-backend = "poetry.core.masonry.api"
43
+
44
+ [tool.black]
45
+ line-length = 88
46
+ target-version = ['py310']
47
+
48
+ [tool.ruff]
49
+ line-length = 88
50
+
51
+ [tool.ruff.lint]
52
+ select = ["E", "F", "W", "I", "N", "UP", "B", "C4"]
53
+
54
+ [tool.mypy]
55
+ python_version = "3.10"
56
+ strict = true
57
+ ignore_missing_imports = true
58
+
59
+ [tool.pytest.ini_options]
60
+ testpaths = ["tests"]
61
+ asyncio_mode = "auto"