agentconnect-cli 0.1.0a1__py3-none-any.whl

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.
Files changed (64) hide show
  1. agentconnect/__init__.py +3 -0
  2. agentconnect/a2a_adapter/__init__.py +6 -0
  3. agentconnect/a2a_adapter/adapter.py +565 -0
  4. agentconnect/a2a_adapter/cards.py +149 -0
  5. agentconnect/a2a_adapter/convert.py +157 -0
  6. agentconnect/a2a_adapter/facade.py +560 -0
  7. agentconnect/a2a_adapter/seen.py +49 -0
  8. agentconnect/a2a_adapter/serve.py +67 -0
  9. agentconnect/adapter.py +319 -0
  10. agentconnect/adapters.py +52 -0
  11. agentconnect/apiclient.py +51 -0
  12. agentconnect/backoff.py +42 -0
  13. agentconnect/callback.py +314 -0
  14. agentconnect/cli.py +2324 -0
  15. agentconnect/cli_json.py +470 -0
  16. agentconnect/cli_support.py +444 -0
  17. agentconnect/clock.py +161 -0
  18. agentconnect/config.py +1213 -0
  19. agentconnect/daemon/__init__.py +41 -0
  20. agentconnect/deliveries.py +295 -0
  21. agentconnect/directory_client.py +255 -0
  22. agentconnect/failpoints.py +99 -0
  23. agentconnect/identity.py +111 -0
  24. agentconnect/isolation.py +345 -0
  25. agentconnect/keystore.py +279 -0
  26. agentconnect/limits.py +457 -0
  27. agentconnect/log.py +143 -0
  28. agentconnect/mcp/__init__.py +30 -0
  29. agentconnect/mcp/backend.py +405 -0
  30. agentconnect/mcp/server.py +382 -0
  31. agentconnect/mcp/session.py +373 -0
  32. agentconnect/mcp/tools.py +583 -0
  33. agentconnect/ownerfile.py +238 -0
  34. agentconnect/payloads.py +109 -0
  35. agentconnect/policy.py +1066 -0
  36. agentconnect/provision.py +239 -0
  37. agentconnect/py.typed +0 -0
  38. agentconnect/relay.py +405 -0
  39. agentconnect/runtime/__init__.py +46 -0
  40. agentconnect/runtime/agent.py +225 -0
  41. agentconnect/runtime/api.py +2043 -0
  42. agentconnect/runtime/engine.py +5359 -0
  43. agentconnect/runtime/errors.py +47 -0
  44. agentconnect/runtime/floor.py +649 -0
  45. agentconnect/runtime/groups.py +1351 -0
  46. agentconnect/runtime/knocks.py +1422 -0
  47. agentconnect/runtime/ledger.py +94 -0
  48. agentconnect/runtime/listing.py +1307 -0
  49. agentconnect/runtime/registry.py +53 -0
  50. agentconnect/runtime/runtime.py +158 -0
  51. agentconnect/runtime/sas.py +1002 -0
  52. agentconnect/sas_emoji.py +83 -0
  53. agentconnect/schemas.py +381 -0
  54. agentconnect/shapes.py +56 -0
  55. agentconnect/sockets.py +86 -0
  56. agentconnect/sse.py +234 -0
  57. agentconnect/store.py +3282 -0
  58. agentconnect/tasks.py +35 -0
  59. agentconnect/tokens.py +63 -0
  60. agentconnect/webhook.py +199 -0
  61. agentconnect_cli-0.1.0a1.dist-info/METADATA +103 -0
  62. agentconnect_cli-0.1.0a1.dist-info/RECORD +64 -0
  63. agentconnect_cli-0.1.0a1.dist-info/WHEEL +4 -0
  64. agentconnect_cli-0.1.0a1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,279 @@
1
+ """Secrets at rest: the OS keyring when available, else 0600 key files.
2
+
3
+ The keystore holds the identity key, the instance key, the DB key and
4
+ invite secrets. Values never appear in logs, ``repr`` or error messages.
5
+
6
+ Files and directories the daemon creates are private: directories 0700,
7
+ files 0600 (see :func:`private_dir` and :func:`private_file`).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import base64
13
+ import contextlib
14
+ import fcntl
15
+ import hashlib
16
+ import os
17
+ import re
18
+ import tempfile
19
+ from pathlib import Path
20
+ from typing import Protocol, override
21
+
22
+ import keyring
23
+ import keyring.backend
24
+ import keyring.errors
25
+
26
+ SERVICE = "agentconnect"
27
+ _NAME = re.compile(r"^[a-z][a-z0-9_.-]{0,40}(/[A-Za-z0-9_-]{1,64})?$")
28
+
29
+
30
+ class KeystoreError(RuntimeError):
31
+ """The keystore could not read or write a secret."""
32
+
33
+
34
+ class KeystoreNameError(KeystoreError):
35
+ """A secret name the keystore refuses (not the store's state, the name's)."""
36
+
37
+
38
+ def private_dir(path: Path) -> Path:
39
+ """Create ``path`` (and parents) and make it 0700."""
40
+ path.mkdir(parents=True, exist_ok=True, mode=0o700)
41
+ path.chmod(0o700)
42
+ return path
43
+
44
+
45
+ def private_file(path: Path) -> Path:
46
+ """Create ``path`` empty if missing and make it 0600."""
47
+ private_dir(path.parent)
48
+ fd = os.open(path, os.O_CREAT | os.O_WRONLY, 0o600)
49
+ os.close(fd)
50
+ path.chmod(0o600)
51
+ return path
52
+
53
+
54
+ def write_private(path: Path, data: bytes) -> None:
55
+ """Atomically replace ``path`` with ``data``, mode 0600: write a fresh
56
+ temporary file (unique name, created 0600) in the same directory, fsync
57
+ it, then ``os.replace`` it into place."""
58
+ private_dir(path.parent)
59
+ fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.", suffix=".tmp")
60
+ try:
61
+ try:
62
+ os.fchmod(fd, 0o600)
63
+ view = memoryview(data)
64
+ while view:
65
+ view = view[os.write(fd, view) :]
66
+ os.fsync(fd)
67
+ finally:
68
+ os.close(fd)
69
+ os.replace(tmp, path)
70
+ except BaseException:
71
+ Path(tmp).unlink(missing_ok=True)
72
+ raise
73
+
74
+
75
+ class DataDirLockedError(RuntimeError):
76
+ """Another runtime (a daemon, an embedded agent or an MCP server) holds
77
+ the data directory. ``holder`` is the kind its note names (``daemon``,
78
+ ``embedded``, ``mcp``), or ``None`` when the note is missing."""
79
+
80
+ def __init__(self, message: str, holder: str | None = None) -> None:
81
+ super().__init__(message)
82
+ self.holder = holder
83
+
84
+
85
+ class DataDirAccessError(OSError):
86
+ """This OS user can't open the data directory's lock (another user's,
87
+ such as a daemon in separate-OS-user mode)."""
88
+
89
+
90
+ LOCK_HOLDERS = {"daemon": "a daemon", "embedded": "an embedded agent", "mcp": "an MCP server"}
91
+
92
+
93
+ class DataDirLock:
94
+ """An exclusive ``flock`` on ``<data_dir>/daemon.lock``, held for the
95
+ runtime's lifetime, so two runtimes (a daemon, an embedded agent, or a
96
+ tool that opens the same state) never share keys, databases or a mailbox.
97
+
98
+ The holder writes ``<owner> <pid>`` into the file, so a second runtime
99
+ can say who has the directory. That note is informational only: the
100
+ ``flock`` is the lock. ``note=False`` (the CLI's identity commands,
101
+ which are no runtime) leaves the last runtime's note as it is."""
102
+
103
+ FILE = "daemon.lock"
104
+
105
+ def __init__(self, data_dir: Path, owner: str = "daemon", *, note: bool = True) -> None:
106
+ private_dir(data_dir)
107
+ path = data_dir / self.FILE
108
+ self._fd: int | None = os.open(path, os.O_CREAT | os.O_RDWR, 0o600)
109
+ try:
110
+ fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
111
+ except OSError:
112
+ held = self._note(self._fd)
113
+ os.close(self._fd)
114
+ self._fd = None
115
+ holder = "unknown" if held is None else f"{LOCK_HOLDERS[held[0]]}, pid {held[1]}"
116
+ msg = (
117
+ f"another agentconnect runtime ({holder}) is using {data_dir}: "
118
+ "stop it, or use another data directory"
119
+ )
120
+ raise DataDirLockedError(msg, None if held is None else held[0]) from None
121
+ if note:
122
+ os.ftruncate(self._fd, 0)
123
+ os.pwrite(self._fd, f"{owner} {os.getpid()}\n".encode("ascii"), 0)
124
+
125
+ @classmethod
126
+ def probe(cls, data_dir: Path) -> tuple[bool, str | None]:
127
+ """Whether a runtime holds ``data_dir`` now, and the kind its note
128
+ names (the holder's, or the last holder's when nobody holds it;
129
+ ``None`` without a note). Takes the lock for an instant when it is
130
+ free, so a runtime that starts at that instant is refused once."""
131
+ try:
132
+ fd = os.open(data_dir / cls.FILE, os.O_RDWR)
133
+ except FileNotFoundError:
134
+ return False, None
135
+ except PermissionError:
136
+ msg = f"{data_dir} is not accessible to this user"
137
+ raise DataDirAccessError(msg) from None
138
+ try:
139
+ note = cls._note(fd)
140
+ try:
141
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
142
+ except OSError:
143
+ return True, None if note is None else note[0]
144
+ fcntl.flock(fd, fcntl.LOCK_UN)
145
+ return False, None if note is None else note[0]
146
+ finally:
147
+ os.close(fd)
148
+
149
+ @staticmethod
150
+ def _note(fd: int) -> tuple[str, str] | None:
151
+ """``(kind, pid)`` from the holder's note, or ``None`` when it's
152
+ missing or not ours."""
153
+ words = os.pread(fd, 64, 0).decode("ascii", "replace").split()
154
+ if len(words) != 2 or words[0] not in LOCK_HOLDERS or not words[1].isdigit(): # noqa: PLR2004
155
+ return None
156
+ return words[0], words[1]
157
+
158
+ def release(self) -> None:
159
+ if self._fd is not None:
160
+ fcntl.flock(self._fd, fcntl.LOCK_UN)
161
+ os.close(self._fd)
162
+ self._fd = None
163
+
164
+
165
+ def _check_name(name: str) -> str:
166
+ if not _NAME.match(name):
167
+ msg = "invalid secret name"
168
+ raise KeystoreNameError(msg)
169
+ return name
170
+
171
+
172
+ class Keystore(Protocol):
173
+ """Named secrets (bytes)."""
174
+
175
+ kind: str
176
+
177
+ def get(self, name: str) -> bytes | None: ...
178
+ def put(self, name: str, value: bytes) -> None: ...
179
+ def delete(self, name: str) -> None: ...
180
+
181
+
182
+ class FileKeystore:
183
+ """One 0600 file per secret in a 0700 directory."""
184
+
185
+ kind = "file"
186
+
187
+ def __init__(self, directory: Path) -> None:
188
+ self._dir = private_dir(directory)
189
+
190
+ @override
191
+ @override
192
+ def __repr__(self) -> str:
193
+ return "FileKeystore(...)"
194
+
195
+ def _path(self, name: str) -> Path:
196
+ return self._dir / _check_name(name).replace("/", "__")
197
+
198
+ def get(self, name: str) -> bytes | None:
199
+ p = self._path(name)
200
+ try:
201
+ return p.read_bytes()
202
+ except FileNotFoundError:
203
+ return None
204
+
205
+ def put(self, name: str, value: bytes) -> None:
206
+ write_private(self._path(name), value)
207
+
208
+ def delete(self, name: str) -> None:
209
+ self._path(name).unlink(missing_ok=True)
210
+
211
+
212
+ class KeyringKeystore:
213
+ """The OS keyring (Secret Service, macOS Keychain, Windows Credential Locker).
214
+
215
+ Entries live under service ``agentconnect``, with a user name scoped to
216
+ the data directory so several daemons on one host don't collide.
217
+ """
218
+
219
+ kind = "keyring"
220
+
221
+ def __init__(self, scope: str, backend: keyring.backend.KeyringBackend | None = None) -> None:
222
+ self._scope = hashlib.sha256(scope.encode()).hexdigest()[:16]
223
+ self._backend = backend or keyring.get_keyring()
224
+
225
+ @override
226
+ @override
227
+ def __repr__(self) -> str:
228
+ return "KeyringKeystore(...)"
229
+
230
+ def _user(self, name: str) -> str:
231
+ return f"{self._scope}/{_check_name(name)}"
232
+
233
+ def get(self, name: str) -> bytes | None:
234
+ try:
235
+ raw = self._backend.get_password(SERVICE, self._user(name))
236
+ except keyring.errors.KeyringError:
237
+ msg = "keyring read failed"
238
+ raise KeystoreError(msg) from None
239
+ return None if raw is None else base64.b64decode(raw)
240
+
241
+ def put(self, name: str, value: bytes) -> None:
242
+ try:
243
+ self._backend.set_password(SERVICE, self._user(name), base64.b64encode(value).decode())
244
+ except keyring.errors.KeyringError:
245
+ msg = "keyring write failed"
246
+ raise KeystoreError(msg) from None
247
+
248
+ def delete(self, name: str) -> None:
249
+ with contextlib.suppress(keyring.errors.PasswordDeleteError):
250
+ self._backend.delete_password(SERVICE, self._user(name))
251
+
252
+
253
+ def keyring_usable(backend: keyring.backend.KeyringBackend | None = None) -> bool:
254
+ """Whether a real keyring backend is configured (not the fail/null ones)."""
255
+ backend = backend or keyring.get_keyring()
256
+ module = type(backend).__module__
257
+ if module.startswith(("keyring.backends.fail", "keyring.backends.null")):
258
+ return False
259
+ try:
260
+ return float(backend.priority) > 0
261
+ except Exception:
262
+ return False
263
+
264
+
265
+ def open_keystore(
266
+ kind: str,
267
+ data_dir: Path,
268
+ backend: keyring.backend.KeyringBackend | None = None,
269
+ ) -> Keystore:
270
+ """``auto`` picks the keyring when usable, otherwise key files."""
271
+ if kind == "file" or (kind == "auto" and not keyring_usable(backend)):
272
+ return FileKeystore(data_dir / "keys")
273
+ if kind in ("keyring", "auto"):
274
+ if not keyring_usable(backend):
275
+ msg = "no usable OS keyring"
276
+ raise KeystoreError(msg)
277
+ return KeyringKeystore(str(data_dir.resolve()), backend)
278
+ msg = f"unknown keystore {kind!r}"
279
+ raise KeystoreError(msg)