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,649 @@
1
+ """Floor control in ``moderated`` groups (SPEC 7.3, 9.3; docs/groups.md).
2
+
3
+ In a ``moderated`` group the instances of the **floor holder** and of the
4
+ **effective moderator** are triggered (:func:`agentconnect.runtime.engine.
5
+ addressed_to_me`). The effective moderator is ``floor_moderator`` while it is
6
+ a member, otherwise the owner.
7
+
8
+ **A register every member computes the same.** The holder is the result of
9
+ folding the valid writes in key order ``(epoch, counter, sender identity,
10
+ sender instance)``. Members that have seen the same writes have the same
11
+ holder, whatever order the writes reached them in:
12
+
13
+ * **Grants.** ``control.floor{holder, counter}``; ``counter`` orders the
14
+ grants of one epoch and starts again at each epoch (at most
15
+ :data:`~agentconnect.schemas.MAX_FLOOR_COUNTER`). The grant's epoch is the
16
+ MLS epoch it was sent in, and it is judged against the **floor facts of
17
+ that epoch** (the mode, the owner, the effective moderator, the member
18
+ identities), never against the receiver's current view. Each member keeps
19
+ the facts of the epochs whose messages it can still decrypt (the current
20
+ one and :data:`PAST_EPOCHS` before it); a grant from an epoch with no
21
+ facts is ignored, the same way everywhere. A valid grant is a write
22
+ whatever this member's own delivery policy decides for the message
23
+ (:meth:`FloorService.accept`). The sender takes its own grant as it seals
24
+ it (:meth:`FloorService.seal`), in the transaction that stores the
25
+ envelope, and keeps that epoch until the relay has taken it.
26
+ * **Resets.** A commit that changes ``floor`` or ``floor_moderator`` writes
27
+ nobody at ``(new epoch, 0)``; a commit after which an identity has no
28
+ instance writes "drop it" at ``(new epoch, 0)``, which clears the holder if
29
+ it names that identity at that point of the order. A grant from before the
30
+ commit loses to them however late it arrives.
31
+ * **Copies.** After a commit that adds instances, one instance (the copier)
32
+ sends the newcomers the current value with the key of the write that set
33
+ it (``copy``, numbered by ``seq`` for that join), and sends it again
34
+ whenever a write from before their epoch, which they can't decrypt,
35
+ changes it, nobody included. A copy is a snapshot: the instances that
36
+ joined in its epoch fold their own writes over the latest one, and
37
+ everyone else ignores it.
38
+
39
+ Any other grant is dropped silently. Writes older than the decryptable
40
+ epochs are folded into a base value, and of one epoch's grants only the
41
+ greatest is kept. Each change is reported as ``floor_changed``.
42
+
43
+ Members still disagree when the relay drops a grant (SPEC 12); that changes
44
+ only who is triggered.
45
+ """
46
+
47
+ from __future__ import annotations
48
+
49
+ import json
50
+ from collections.abc import Iterable
51
+ from typing import Any
52
+
53
+ from agentconnect import log
54
+ from agentconnect.relay import unb64
55
+ from agentconnect.runtime.engine import DaemonError, Engine
56
+ from agentconnect.schemas import MAX_FLOOR_COUNTER, normalize
57
+
58
+ LOG = log.get("agentconnect.floor")
59
+
60
+ FLOOR_KIND = "control.floor"
61
+ # Past epochs whose application messages members can still decrypt (the
62
+ # core's MAX_PAST_EPOCHS, the relay's one-epoch grace window, SPEC 8.3).
63
+ PAST_EPOCHS = 1
64
+
65
+ Key = tuple[int, int, str, str] # (epoch, counter, sender identity, sender instance)
66
+ Write = tuple[int, int, str, str, str, str | None] # key, kind, holder
67
+
68
+
69
+ def effective_moderator(policy: dict[str, Any], identities: Iterable[str]) -> str | None:
70
+ """``floor_moderator`` while it is a member, otherwise the owner (SPEC 9.3)."""
71
+ moderator = policy.get("floor_moderator")
72
+ if isinstance(moderator, str) and moderator in set(identities):
73
+ return moderator
74
+ owner = policy.get("owner")
75
+ return owner if isinstance(owner, str) else None
76
+
77
+
78
+ def facts_of(policy: dict[str, Any], identities: Iterable[str]) -> dict[str, Any]:
79
+ """The floor facts of an epoch: what a grant sent in it is judged on."""
80
+ members = sorted(set(identities))
81
+ return {
82
+ "mode": policy.get("floor"),
83
+ "moderator": policy.get("floor_moderator"),
84
+ "owner": policy.get("owner"),
85
+ "effective": effective_moderator(policy, members),
86
+ "identities": members,
87
+ }
88
+
89
+
90
+ def may_grant(facts: dict[str, Any], identity: str) -> bool:
91
+ """Whether ``identity`` may grant under ``facts``: the floor is
92
+ ``moderated`` and it is the effective moderator or the owner (SPEC 7.3)."""
93
+ return bool(
94
+ facts.get("mode") == "moderated" and identity in (facts["effective"], facts["owner"])
95
+ )
96
+
97
+
98
+ def floor_write(
99
+ facts: dict[str, Any] | None, epoch: int, sender: str, instance: str, body: dict[str, Any]
100
+ ) -> tuple[Key, str | None] | None:
101
+ """The register write a ``control.floor`` sent in ``epoch`` makes, judged
102
+ on that epoch's ``facts``: ``(key, holder)``, or ``None`` if it is not
103
+ valid. ``body`` is in its schema's shape."""
104
+ if facts is None or not may_grant(facts, sender):
105
+ return None
106
+ holder = _opt_str(body.get("holder"))
107
+ if holder is not None and holder not in facts["identities"]:
108
+ return None
109
+ counter = int(body["counter"])
110
+ copy = body.get("copy")
111
+ if copy is None:
112
+ return (epoch, counter, sender, instance), holder
113
+ if int(copy["epoch"]) > epoch:
114
+ return None
115
+ return (int(copy["epoch"]), counter, str(copy["sender"]), str(copy["instance"])), holder
116
+
117
+
118
+ def fold(
119
+ holder: str | None, source: Key | None, writes: Iterable[Write]
120
+ ) -> tuple[str | None, Key | None]:
121
+ """The register after ``writes`` (in key order) over a base value: the
122
+ holder and the key of the write that last set it. A write at or below the
123
+ base's key is part of the base already."""
124
+ for epoch, counter, sender, instance, kind, value in writes:
125
+ if source is not None and (epoch, counter, sender, instance) <= source:
126
+ continue
127
+ if kind in {"grant", "reset"} or (kind == "drop" and holder == value):
128
+ holder = value if kind == "grant" else None
129
+ source = (epoch, counter, sender, instance)
130
+ return holder, source
131
+
132
+
133
+ def floor_event(facts: dict[str, Any], holder: str | None, by: str, reason: str) -> dict[str, Any]:
134
+ """The data of a ``floor_changed`` event."""
135
+ return {
136
+ "mode": facts["mode"],
137
+ "moderator": facts["moderator"],
138
+ "effective_moderator": facts["effective"],
139
+ "holder": holder,
140
+ "by": by,
141
+ "reason": reason,
142
+ }
143
+
144
+
145
+ def _opt_str(value: Any) -> str | None:
146
+ return value if isinstance(value, str) else None
147
+
148
+
149
+ def _key(raw: Any) -> Key | None:
150
+ return None if raw is None else (int(raw[0]), int(raw[1]), str(raw[2]), str(raw[3]))
151
+
152
+
153
+ class FloorService:
154
+ """The floor register of each group of one :class:`Engine`."""
155
+
156
+ def __init__(self, engine: Engine) -> None:
157
+ self._engine = engine
158
+
159
+ # -- state ----------------------------------------------------------------
160
+
161
+ def _now_facts(self, group_id: str) -> dict[str, Any]:
162
+ client = self._engine.client
163
+ gid = unb64(group_id)
164
+ policy = dict(json.loads(client.policy(gid)))
165
+ return facts_of(policy, (str(m["identity"]) for m in client.members(gid)))
166
+
167
+ def _epoch(self, group_id: str) -> int:
168
+ return int(self._engine.client.epoch(unb64(group_id)))
169
+
170
+ def _load(self, group_id: str) -> tuple[str | None, dict[str, Any]]:
171
+ """The holder and the register state; a group without a row (just
172
+ created or joined, or from before floor control) starts with nobody,
173
+ and the facts of its current epoch."""
174
+ row = self._engine.store.floor(group_id)
175
+ if row is not None:
176
+ holder, state = row
177
+ for field in ("counters", "pending", "joins"):
178
+ state.setdefault(field, {})
179
+ return holder, state
180
+ facts = self._now_facts(group_id)
181
+ state = {
182
+ "counters": {},
183
+ "pending": {},
184
+ "joins": {},
185
+ "base": None,
186
+ "bound": None,
187
+ "source": None,
188
+ "facts": {str(self._epoch(group_id)): facts},
189
+ "reported": {
190
+ "mode": facts["mode"],
191
+ "moderator": facts["moderator"],
192
+ "effective": facts["effective"],
193
+ "holder": None,
194
+ },
195
+ }
196
+ return None, state
197
+
198
+ def _facts(self, group_id: str, state: dict[str, Any], epoch: int) -> dict[str, Any] | None:
199
+ """The facts of ``epoch``, if kept. The current epoch's are taken
200
+ from the group when missing (a group from before floor control)."""
201
+ facts = state["facts"].get(str(epoch))
202
+ if facts is None and epoch == self._epoch(group_id):
203
+ facts = state["facts"][str(epoch)] = self._now_facts(group_id)
204
+ return facts
205
+
206
+ def ensure(self, group_id: str) -> None:
207
+ """Give a group that has no floor row one, with nobody holding the
208
+ floor: a group this runtime created or joined starts so."""
209
+ if self._engine.store.floor(group_id) is None:
210
+ holder, state = self._load(group_id)
211
+ self._engine.store.set_floor(group_id, holder, state)
212
+
213
+ def holder(self, group_id: str) -> str | None:
214
+ row = self._engine.store.floor(group_id)
215
+ return None if row is None else row[0]
216
+
217
+ def view(self, group_id: str, policy: dict[str, Any], active: bool) -> dict[str, Any]:
218
+ """The group view's ``floor``: the policy's mode, pin and moderator,
219
+ the effective moderator and the holder (both ``None`` for a group
220
+ that isn't active)."""
221
+ return {
222
+ "mode": policy.get("floor"),
223
+ "pinned": bool(policy.get("floor_pinned")),
224
+ "moderator": policy.get("floor_moderator"),
225
+ "effective_moderator": self._now_facts(group_id)["effective"] if active else None,
226
+ "holder": self.holder(group_id) if active else None,
227
+ }
228
+
229
+ def triggers(self, group_id: str, policy: dict[str, Any]) -> tuple[str | None, str | None]:
230
+ """``(effective moderator, holder)``: whom ``moderated`` triggers."""
231
+ if policy.get("floor") != "moderated":
232
+ return None, None
233
+ return _opt_str(self._now_facts(group_id)["effective"]), self.holder(group_id)
234
+
235
+ def _fold(self, group_id: str, state: dict[str, Any]) -> str | None:
236
+ base = state["base"] or {"holder": None, "source": None}
237
+ holder, source = fold(
238
+ base["holder"], _key(base["source"]), self._engine.store.floor_writes(group_id)
239
+ )
240
+ state["source"] = None if source is None else list(source)
241
+ return holder
242
+
243
+ def _settled(self, group_id: str, state: dict[str, Any]) -> tuple[str | None, Key | None]:
244
+ """The register without this instance's own pending grants, which may
245
+ still be taken back: what a copy carries, so a copy never spreads a
246
+ grant that never went out."""
247
+ pending = {tuple(k) for k in state["pending"].values()}
248
+ base = state["base"] or {"holder": None, "source": None}
249
+ writes = [w for w in self._engine.store.floor_writes(group_id) if w[:4] not in pending]
250
+ return fold(base["holder"], _key(base["source"]), writes)
251
+
252
+ def _merge(
253
+ self, group_id: str, state: dict[str, Any], key: Key, kind: str, value: str | None
254
+ ) -> str | None:
255
+ """Keep a write and return the register's holder after it. A write
256
+ older than what was folded away is not kept. Of the grants of one
257
+ epoch only the greatest is kept: it alone decides the fold, so every
258
+ member keeps the same set whatever order the grants came in."""
259
+ store = self._engine.store
260
+ bound = _key(state["bound"])
261
+ if bound is not None and key <= bound:
262
+ return self._fold(group_id, state)
263
+ store.add_floor_write(group_id, key, kind, value)
264
+ counters = state["counters"]
265
+ counters[str(key[0])] = max(int(counters.get(str(key[0]), 0)), key[1])
266
+ if kind == "grant":
267
+ self._prune(group_id, state, key[0])
268
+ return self._fold(group_id, state)
269
+
270
+ def _prune(self, group_id: str, state: dict[str, Any], epoch: int) -> None:
271
+ """Keep, of an epoch's grants, the greatest, the greatest that isn't
272
+ this instance's own pending one (which may still be taken back) and
273
+ the pending ones: the rest can never decide the fold again."""
274
+ store = self._engine.store
275
+ pending = {tuple(k) for k in state["pending"].values()}
276
+ grants = [w[:4] for w in store.floor_writes(group_id) if w[0] == epoch and w[4] == "grant"]
277
+ keep = set(pending)
278
+ if grants:
279
+ keep.add(max(grants))
280
+ final = [g for g in grants if g not in pending]
281
+ if final:
282
+ keep.add(max(final))
283
+ for g in grants:
284
+ if g not in keep:
285
+ store.delete_floor_write(group_id, g)
286
+
287
+ def _report(
288
+ self, state: dict[str, Any], facts: dict[str, Any], holder: str | None, by: str, reason: str
289
+ ) -> dict[str, Any] | None:
290
+ """``floor_changed`` data when the holder, mode, moderator or effective
291
+ moderator differs from what was last reported."""
292
+ now = {**{k: facts[k] for k in ("mode", "moderator", "effective")}, "holder": holder}
293
+ if state["reported"] == now:
294
+ return None
295
+ state["reported"] = now
296
+ return floor_event(facts, holder, by, reason)
297
+
298
+ # -- grants -----------------------------------------------------------------
299
+
300
+ def judge(
301
+ self, group_id: str, epoch: int, sender: str, instance: str, inner: dict[str, Any]
302
+ ) -> tuple[Key, str | None] | None:
303
+ """The write a ``control.floor`` sent in ``epoch`` makes, or ``None``
304
+ when it isn't valid (see the module docs)."""
305
+ _, state = self._load(group_id)
306
+ return self._judge(group_id, state, epoch, sender, instance, inner)
307
+
308
+ def _judge( # noqa: PLR0913, PLR0917
309
+ self,
310
+ group_id: str,
311
+ state: dict[str, Any],
312
+ epoch: int,
313
+ sender: str,
314
+ instance: str,
315
+ inner: dict[str, Any],
316
+ ) -> tuple[Key, str | None] | None:
317
+ body = inner.get("body")
318
+ shaped = normalize(FLOOR_KIND, body) if isinstance(body, dict) else None
319
+ if shaped is None:
320
+ return None
321
+ return floor_write(self._facts(group_id, state, epoch), epoch, sender, instance, shaped)
322
+
323
+ def _recopy(
324
+ self,
325
+ group_id: str,
326
+ state: dict[str, Any],
327
+ settled: tuple[str | None, Key | None],
328
+ key: Key,
329
+ ) -> None:
330
+ """After a write at ``key`` that was ``settled`` before: if the
331
+ settled value changed, tell the instances that joined in a later
332
+ epoch, which can't decrypt the write, if this instance is the one
333
+ that copies to them (SPEC 9.3)."""
334
+ now = self._settled(group_id, state)
335
+ if now == settled:
336
+ return
337
+ for epoch, join in sorted(state["joins"].items()):
338
+ if key[0] < int(epoch) and join["copier"]:
339
+ self._send_copy(group_id, state, now, int(epoch))
340
+
341
+ def _changed(
342
+ self, group_id: str, state: dict[str, Any], holder: str | None, key: Key
343
+ ) -> dict[str, Any] | None:
344
+ """The ``floor_changed`` data after a grant at ``key``."""
345
+ by = key[2] or self._engine.me
346
+ return self._report(state, self._now_facts(group_id), holder, by, "grant")
347
+
348
+ async def accept(
349
+ self, group_id: str, epoch: int, sender: str, instance: str, inner: dict[str, Any]
350
+ ) -> None:
351
+ """Take a received grant (inside its message's transaction), whatever
352
+ the local delivery policy decided for the message: a rate or a size
353
+ limit is the receiver's own, and the register must not depend on it."""
354
+ _, state = self._load(group_id)
355
+ write = self._judge(group_id, state, epoch, sender, instance, inner)
356
+ if write is None:
357
+ return
358
+ key, value = write
359
+ copy = (normalize(FLOOR_KIND, inner["body"]) or {}).get("copy")
360
+ if copy is not None:
361
+ await self._snapshot(group_id, state, copy, (key, value), (sender, instance))
362
+ return
363
+ settled = self._settled(group_id, state)
364
+ holder = self._merge(group_id, state, key, "grant", value)
365
+ self._recopy(group_id, state, settled, key)
366
+ data = self._changed(group_id, state, holder, key)
367
+ self._engine.store.set_floor(group_id, holder, state)
368
+ if data is not None:
369
+ LOG.info("floor_changed", group=log.hid(group_id), reason="grant")
370
+ await self._engine.emit(group_id, "floor_changed", **data)
371
+
372
+ def _join_epoch(self, group_id: str) -> int | None:
373
+ """The epoch this instance joined the group in (first seen, G8)."""
374
+ mailbox = self._engine.mailbox
375
+ for m in self._engine.store.members(group_id):
376
+ for i in m.instances:
377
+ if i.get("mailbox_id") == mailbox:
378
+ return int(i.get("joined_epoch", 0))
379
+ return None
380
+
381
+ async def _snapshot(
382
+ self,
383
+ group_id: str,
384
+ state: dict[str, Any],
385
+ copy: dict[str, Any],
386
+ write: tuple[Key, str | None],
387
+ sender: tuple[str, str],
388
+ ) -> None:
389
+ """A copy (SPEC 9.3) is a snapshot for the instances that joined in
390
+ its ``join`` epoch, which can't decrypt what came before: the latest
391
+ one (by ``seq``, then sender) replaces the base the register folds
392
+ from. Everyone else holds the real writes and ignores it. Once this
393
+ instance has folded writes of its own away, its base is its own."""
394
+ if int(copy["join"]) != self._join_epoch(group_id) or state["bound"] is not None:
395
+ return
396
+ rank = [int(copy["seq"]), *sender]
397
+ last = state.get("snapshot")
398
+ if last is not None and rank <= last:
399
+ return
400
+ key, value = write
401
+ state["snapshot"] = rank
402
+ state["base"] = {"holder": value, "source": list(key)}
403
+ holder = self._fold(group_id, state)
404
+ data = self._changed(group_id, state, holder, key)
405
+ self._engine.store.set_floor(group_id, holder, state)
406
+ if data is not None:
407
+ LOG.info("floor_changed", group=log.hid(group_id), reason="grant")
408
+ await self._engine.emit(group_id, "floor_changed", **data)
409
+
410
+ def seal(self, group_id: str, payload: dict[str, Any], epoch: int) -> Any:
411
+ """This instance seals its own ``control.floor`` in ``epoch`` (inside
412
+ the transaction that stores the sealed envelope): a grant gets the
413
+ epoch's next counter, and the write is taken now, by the receivers'
414
+ rule, so a crash after the relay took it loses nothing. It stays
415
+ pending, with its epoch's facts kept, until the relay confirms it
416
+ (:meth:`confirmed`); a reseal in a later epoch replaces it, and a send
417
+ that fails takes it back (:meth:`unsent`). Returns the event to tell."""
418
+ e = self._engine
419
+ body = payload["inner"]["body"]
420
+ if "copy" in body:
421
+ return None # a copy of what this instance holds: nothing to take
422
+ _, state = self._load(group_id)
423
+ msg_id = str(payload["msg_id"])
424
+ self._take_back(group_id, state, msg_id)
425
+ body["counter"] = min(int(state["counters"].get(str(epoch), 0)) + 1, MAX_FLOOR_COUNTER)
426
+ me, instance = e.me, e.instance.instance_id_hex
427
+ write = self._judge(group_id, state, epoch, me, instance, payload["inner"])
428
+ key: Key = (epoch, 0, me, instance)
429
+ if write is not None:
430
+ key, value = write
431
+ state["pending"][msg_id] = list(key)
432
+ holder = self._merge(group_id, state, key, "grant", value)
433
+ else:
434
+ holder = self._fold(group_id, state) # a reseal may have taken one back
435
+ data = self._changed(group_id, state, holder, key)
436
+ e.store.set_floor(group_id, holder, state)
437
+ if data is None:
438
+ return None
439
+ return e._record(group_id, "floor_changed", f"floor_sent/{msg_id}/{epoch}", data)
440
+
441
+ def _take_back(self, group_id: str, state: dict[str, Any], msg_id: str) -> None:
442
+ key = _key(state["pending"].pop(msg_id, None))
443
+ if key is not None:
444
+ self._engine.store.delete_floor_write(group_id, key)
445
+
446
+ def confirmed(self, group_id: str, msg_id: str) -> None:
447
+ """The relay took this instance's ``control.floor``: it is final, and
448
+ the instances that joined since it was sealed, which can't decrypt
449
+ it, get a copy if it changes the settled value."""
450
+ e = self._engine
451
+ if e.store.floor(group_id) is None:
452
+ return
453
+ with e.store.tx():
454
+ holder, state = self._load(group_id)
455
+ key = _key(state["pending"].get(msg_id))
456
+ if key is None:
457
+ return
458
+ settled = self._settled(group_id, state)
459
+ state["pending"].pop(msg_id)
460
+ self._prune(group_id, state, key[0])
461
+ self._recopy(group_id, state, settled, key)
462
+ e.store.set_floor(group_id, holder, state)
463
+
464
+ def unsent(self, group_id: str, msg_id: str) -> Any:
465
+ """A sealed ``control.floor`` of this instance will never go out: take
466
+ its write back. Returns the event to tell."""
467
+ e = self._engine
468
+ if e.store.group(group_id) is None:
469
+ return None
470
+ _, state = self._load(group_id)
471
+ key = _key(state["pending"].get(msg_id))
472
+ if key is None:
473
+ return None
474
+ self._take_back(group_id, state, msg_id) # copies never carried it
475
+ holder = self._fold(group_id, state)
476
+ data = self._changed(group_id, state, holder, key)
477
+ e.store.set_floor(group_id, holder, state)
478
+ if data is None:
479
+ return None
480
+ return e._record(group_id, "floor_changed", f"floor_unsent/{msg_id}", data)
481
+
482
+ def reconcile(self) -> list[Any]:
483
+ """At start: a pending own grant whose outbox row failed, or is gone,
484
+ never went out, so take it back; one whose row is done went out.
485
+ Returns the events to tell."""
486
+ e = self._engine
487
+ events = []
488
+ for group in e.store.groups():
489
+ row = e.store.floor(group.group_id)
490
+ if group.status != "active" or row is None:
491
+ continue
492
+ for msg_id in list(row[1].get("pending", {})):
493
+ state = e.store.outbox_state_of(msg_id)
494
+ if state == "done":
495
+ self.confirmed(group.group_id, msg_id)
496
+ elif state in (None, "failed"):
497
+ with e.store.tx():
498
+ event = self.unsent(group.group_id, msg_id)
499
+ if event is not None:
500
+ events.append(event)
501
+ return events
502
+
503
+ def grant(self, group_id: str, holder: str | None, *, origin: str) -> dict[str, Any]:
504
+ """Grant the floor to ``holder``, or release it (``None``): send
505
+ ``control.floor`` to the group. Only the effective moderator or the
506
+ owner of a ``moderated`` group may; the grant is taken when it is
507
+ sealed (:meth:`seal`). ``origin`` is ``human`` for the human token,
508
+ else ``agent``, whose grants count against its send rate like its
509
+ messages."""
510
+ e = self._engine
511
+ group_id = e._group_or_raise(group_id).group_id
512
+ facts = self._now_facts(group_id)
513
+ if facts["mode"] != "moderated":
514
+ raise DaemonError("not_moderated", "the group's floor isn't moderated")
515
+ if not may_grant(facts, e.me):
516
+ msg = "only the effective moderator or the owner grants the floor (SPEC 9.3)"
517
+ raise DaemonError("not_permitted", msg)
518
+ if holder is not None and holder not in facts["identities"]:
519
+ raise DaemonError("not_found", "no such member")
520
+ # The counter is the sealing epoch's, set when it is sealed.
521
+ msg_id, _ = e.queue_message(
522
+ group_id,
523
+ {"holder": holder, "counter": 0},
524
+ kind=FLOOR_KIND,
525
+ origin=origin,
526
+ floor_grant=True,
527
+ )
528
+ LOG.info("floor_grant_queued", group=log.hid(group_id))
529
+ return {"status": "granted", "holder": holder, "msg_id": msg_id}
530
+
531
+ # -- commits ----------------------------------------------------------------
532
+
533
+ async def after_commit(self, group_id: str, summary: dict[str, Any], joined: list[str]) -> None:
534
+ """After a merged commit (inside its transaction): keep the new
535
+ epoch's facts, write the resets the commit makes, fold away writes no
536
+ grant can come after any more, report a change, and send newcomers a
537
+ copy of the current value."""
538
+ e = self._engine
539
+ epoch = int(summary["epoch"])
540
+ holder, state = self._load(group_id)
541
+ facts = self._now_facts(group_id)
542
+ if epoch == self._epoch(group_id):
543
+ state["facts"][str(epoch)] = facts
544
+ before = state["facts"].get(str(epoch - 1))
545
+ reason = "moderator"
546
+ if self._floor_changed(summary, before, facts):
547
+ holder = self._merge(group_id, state, (epoch, 0, "", ""), "reset", None)
548
+ reason = "policy"
549
+ gone = {str(m["identity"]) for m in summary.get("removed") or []}
550
+ for identity in sorted(gone - set(facts["identities"])):
551
+ holder = self._merge(group_id, state, (epoch, 0, "", identity), "drop", identity)
552
+ if reason != "policy" and holder != state["reported"].get("holder"):
553
+ reason = "reset"
554
+ if joined:
555
+ copier = self._copier(group_id, summary, facts)
556
+ state["joins"][str(epoch)] = {"identities": sorted(set(joined)), "copier": copier}
557
+ self._fold_old(group_id, state, epoch - PAST_EPOCHS)
558
+ data = self._report(state, facts, holder, str(summary["committer"]["identity"]), reason)
559
+ settled = self._settled(group_id, state)
560
+ if joined and state["joins"][str(epoch)]["copier"] and settled[0] is not None:
561
+ self._send_copy(group_id, state, settled, epoch)
562
+ e.store.set_floor(group_id, holder, state)
563
+ if data is not None:
564
+ await e.emit(group_id, "floor_changed", **data)
565
+
566
+ @staticmethod
567
+ def _floor_changed(
568
+ summary: dict[str, Any], before: dict[str, Any] | None, facts: dict[str, Any]
569
+ ) -> bool:
570
+ """Whether the commit changed ``floor`` or ``floor_moderator``."""
571
+ if not summary.get("policy_changed"):
572
+ return False
573
+ previous = summary.get("previous_policy")
574
+ if previous:
575
+ old = json.loads(previous)
576
+ return (old.get("floor"), old.get("floor_moderator")) != (
577
+ facts["mode"],
578
+ facts["moderator"],
579
+ )
580
+ # A summary journalled before the core reported policies.
581
+ return before is not None and (before["mode"], before["moderator"]) != (
582
+ facts["mode"],
583
+ facts["moderator"],
584
+ )
585
+
586
+ def _fold_old(self, group_id: str, state: dict[str, Any], oldest: int) -> None:
587
+ """Fold the writes of epochs before ``oldest`` into the base value:
588
+ no grant from those epochs can be decrypted any more. An epoch with
589
+ an own grant still pending keeps its writes and facts."""
590
+ store = self._engine.store
591
+ pending = [int(k[0]) for k in state["pending"].values()]
592
+ oldest = min([oldest, *pending])
593
+ old = [w for w in store.floor_writes(group_id) if w[0] < oldest]
594
+ if old:
595
+ base = state["base"] or {"holder": None, "source": None}
596
+ holder, source = fold(base["holder"], _key(base["source"]), old)
597
+ state["base"] = {"holder": holder, "source": None if source is None else list(source)}
598
+ state["bound"] = list(old[-1][:4])
599
+ store.drop_floor_writes(group_id, oldest)
600
+ for field in ("facts", "counters", "joins"):
601
+ state[field] = {k: v for k, v in state[field].items() if int(k) >= oldest}
602
+
603
+ def _copier(self, group_id: str, summary: dict[str, Any], facts: dict[str, Any]) -> bool:
604
+ """Whether this instance copies the floor to the instances a commit
605
+ added: the lowest instance, among those that were in the group before
606
+ the commit, of the effective moderator, or else of the owner (a
607
+ moderator that just joined has no value yet)."""
608
+ e = self._engine
609
+ if facts["mode"] != "moderated":
610
+ return False
611
+ added = {str(m.get("mailbox_id")) for m in summary.get("added") or []}
612
+ members = [m for m in e.client.members(unb64(group_id)) if m["mailbox_id"] not in added]
613
+ senders = [m for m in members if m["identity"] == facts["effective"]] or [
614
+ m for m in members if m["identity"] == facts["owner"]
615
+ ]
616
+ lowest = min(senders, key=lambda m: int(m["leaf_index"]), default=None)
617
+ return lowest is not None and str(lowest["mailbox_id"]) == e.mailbox
618
+
619
+ def _send_copy(
620
+ self,
621
+ group_id: str,
622
+ state: dict[str, Any],
623
+ settled: tuple[str | None, Key | None],
624
+ join: int,
625
+ ) -> None:
626
+ """Send the instances that joined in ``join`` the settled value
627
+ (:meth:`_settled`) with the key of the write that set it, and the
628
+ next ``seq`` of this join's copies: the daemon's own control, hop 0,
629
+ replying to nothing."""
630
+ holder, source = settled
631
+ if source is None:
632
+ return
633
+ record = state["joins"][str(join)]
634
+ record["seq"] = min(int(record.get("seq", 0)) + 1, MAX_FLOOR_COUNTER)
635
+ epoch, counter, sender, instance = source
636
+ copy = {
637
+ "epoch": epoch,
638
+ "sender": sender,
639
+ "instance": instance,
640
+ "join": join,
641
+ "seq": record["seq"],
642
+ }
643
+ self._engine.queue_message(
644
+ group_id,
645
+ {"holder": holder, "counter": counter, "copy": copy},
646
+ to=sorted(set(record["identities"])),
647
+ kind=FLOOR_KIND,
648
+ daemon_control=True,
649
+ )