@passioncode-ai/fabric-agent-adapter 0.82.0 → 0.84.1

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 (165) hide show
  1. package/CHANGELOG.md +169 -3
  2. package/README.md +7 -3
  3. package/SECURITY.md +4 -3
  4. package/package.json +1 -1
  5. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  6. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +7 -2
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +10 -0
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -2
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +18 -4
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +16 -0
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_identifiers.py +1 -0
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +31 -15
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +38 -12
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +21 -148
  15. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check_cli.py +164 -0
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +15 -8
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +4 -3
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +2 -1
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +3 -2
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +14 -4
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/README.md +76 -0
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/config.example.json +28 -0
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/data_agent.py +366 -0
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +32 -17
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +4 -0
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support-agent.mjs +115 -0
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +63 -11
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/a2a.md +98 -0
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +10 -0
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +28 -12
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +1 -1
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -2
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +13 -1
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +11 -7
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/ops-digest.md +75 -0
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +7 -5
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -12
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +10 -1
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +70 -0
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_a2a.py +158 -0
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a-server.mjs +275 -0
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a.mjs +2 -1
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +1 -0
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +5 -2
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +1 -0
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +1 -0
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +15 -2
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +55 -11
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +47 -31
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +1 -0
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +25 -7
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +1 -0
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +27 -5
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +1 -0
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +1 -0
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +4 -2
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +19 -12
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate-rules.mjs +1 -0
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +1 -0
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +7 -1
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +22 -7
  64. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +4 -5
  65. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +7 -2
  66. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-identifiers.mjs +1 -0
  67. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +21 -1
  68. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +6 -0
  69. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +1 -0
  70. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +1 -0
  71. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +16 -2
  72. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +1 -0
  73. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +6 -0
  74. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +25 -3
  75. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ops-digest.mjs +456 -0
  76. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package-schema.mjs +1 -0
  77. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +18 -2
  78. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-page-links.mjs +1 -0
  79. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +19 -5
  80. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +56 -19
  81. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +1 -0
  82. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -0
  83. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -0
  84. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +16 -2
  85. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +1 -0
  86. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -0
  87. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-socket-proof.mjs +181 -0
  88. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +20 -15
  89. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet-identity.mjs +469 -0
  90. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet.mjs +1 -0
  91. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +1 -0
  92. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +1 -0
  93. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a.py +3 -1
  94. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a_server.py +405 -0
  95. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +1 -0
  96. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +7 -2
  97. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +1 -0
  98. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +1 -0
  99. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
  100. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +17 -3
  101. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +98 -9
  102. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +37 -48
  103. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +1 -0
  104. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +25 -8
  105. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +1 -0
  106. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +28 -6
  107. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +1 -0
  108. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +1 -0
  109. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +19 -13
  110. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +24 -14
  111. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +1 -0
  112. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate_rules.py +1 -0
  113. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -1
  114. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +32 -7
  115. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +19 -16
  116. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +10 -5
  117. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_identifiers.py +1 -0
  118. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +56 -96
  119. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +99 -1
  120. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +1 -0
  121. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +1 -0
  122. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +16 -6
  123. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +1 -0
  124. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_mandate.py +1 -0
  125. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +44 -1
  126. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ops_digest.py +648 -0
  127. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +17 -4
  128. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package_schema.py +1 -0
  129. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_page_links.py +1 -0
  130. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +21 -5
  131. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +108 -108
  132. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +1 -0
  133. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +1 -0
  134. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -0
  135. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +38 -12
  136. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +1 -0
  137. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +1 -0
  138. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_socket_proof.py +391 -0
  139. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +19 -16
  140. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet.py +1 -0
  141. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet_identity.py +493 -0
  142. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +1 -0
  143. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -0
  144. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +632 -104
  145. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample-remote-service.mjs +2 -1
  146. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +17 -7
  147. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/tailnet-groups.schema.json +69 -0
  148. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +6 -5
  149. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +16 -0
  150. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +10 -0
  151. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +14 -9
  152. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +4 -2
  153. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +18 -4
  154. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +4 -3
  155. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/admit.py +182 -0
  156. package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
  157. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
  158. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +10 -0
  159. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +14 -9
  160. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +7 -4
  161. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +10 -5
  162. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +2 -0
  163. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +16 -5
  164. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +79 -8
  165. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +147 -45
@@ -1,8 +1,10 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """The board link (DEC-0078, FAA-12b): the board session over the hub, the fsynced checkpoint and a claimed request as
2
3
  data. Part of the board connector (FAA-65, layer L4), below the responder in fabric_board_session, which re-exports these
3
4
  names. Standard library only, Python 3.9+."""
4
5
  from __future__ import annotations
5
6
 
7
+ import inspect
6
8
  import json
7
9
  import math
8
10
  import re
@@ -98,17 +100,29 @@ class BoardSession:
98
100
  return value
99
101
 
100
102
  def close(self, timeout: Optional[float] = None) -> bool:
101
- """Sends the DELETE (waiting `timeout` seconds at most when above 0, else the session's); True when Fabric answered it with a
103
+ """Sends the DELETE (waiting `timeout` seconds at most for the whole of it when above 0, FAA-187 (12), else the
104
+ session's per read); True when Fabric answered it with a
102
105
  2xx, so the slots are free now. False otherwise, a 404 (the session was already gone) included: a move made
103
106
  before it ended may be read only at the next com.status, so the slot records stay; the lease lapses in 60 s."""
104
107
  with self._lock:
105
108
  if self.mcp_session is None:
106
109
  return False
110
+ got: List[Any] = []
111
+
112
+ def send() -> None:
113
+ try:
114
+ got.append(self._send({}, method="DELETE", timeout=timeout))
115
+ except Exception: # noqa: BLE001 - any failure is no release, as the Node twin's catch reads it
116
+ pass
107
117
  try:
108
- answer = self._send({}, method="DELETE", timeout=timeout)
118
+ if timeout is not None and timeout > 0: # the whole DELETE, not each socket read (FAA-187 (12))
119
+ worker = threading.Thread(target=send, name="board-leave", daemon=True)
120
+ worker.start()
121
+ worker.join(timeout)
122
+ else:
123
+ send()
124
+ answer = got[0] if got else None
109
125
  status = answer[0] if isinstance(answer, (tuple, list)) and answer and isinstance(answer[0], int) else 0
110
- except Exception: # noqa: BLE001 - any failure is no release, as the Node twin's catch reads it
111
- status = 0
112
126
  finally:
113
127
  self.mcp_session = None # an answer it cannot read is no release, as the Node twin reads it
114
128
  return 200 <= status < 300
@@ -263,6 +277,10 @@ def _bounded_int(literal: str) -> int:
263
277
  return int(literal)
264
278
 
265
279
 
280
+ #: `mandate/1` as a block may write it: its slash plain or JSON-escaped (`\\/`, `\\u002f`).
281
+ _KIND_WRITTEN = re.compile(r"mandate(?:/|\\/|\\u002[fF])1")
282
+
283
+
266
284
  def read_mandate(request: "BoardRequest", mandate_from: Any, agent_id: Optional[str]) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
267
285
  """The mandate a request carries, read only when the service opted in (`agent_id` and `mandate_from` set), the board
268
286
  verified the sender (`provenance: trusted`) and its Project is one `mandate_from` names (or `PERSONAL_BOARD`): a T1
@@ -285,7 +303,7 @@ def read_mandate(request: "BoardRequest", mandate_from: Any, agent_id: Optional[
285
303
  try:
286
304
  value = json.loads(block, parse_constant=_no_constant, parse_int=_bounded_int)
287
305
  except (ValueError, RecursionError):
288
- if "mandate/1" in block:
306
+ if _KIND_WRITTEN.search(block): # the kind as written, a JSON escape of its slash included (FAA-196 (13))
289
307
  return None, "mandate-unreadable"
290
308
  continue
291
309
  if isinstance(value, dict) and value.get("kind") == "mandate/1":
@@ -300,9 +318,10 @@ CALL_MEMBERS = ("capability", "action", "effect", "spendUsd", "project") # what
300
318
 
301
319
 
302
320
  def covers(mandate: Optional[Dict[str, Any]], call: Any, agent_id: Optional[str], wall: Callable[[], float],
303
- caller: Any = None) -> Optional[Dict[str, Any]]:
304
- """`mandate_covers` for a call under a board request's mandate, its sender placed as T1 and no caller agent id (the
305
- board names a Project's principal, not an agent the chain can bind; the chain's end alone binds it, MD-3). A covered
321
+ caller: Any = None, caller_agent: Optional[str] = None) -> Optional[Dict[str, Any]]:
322
+ """`mandate_covers` for a call under a board request's mandate, its sender placed as T1. The caller's agent id is
323
+ `caller_agent`, what the service's identify_agent hook named for the sender (`sender_agent`, FAA-135); without one
324
+ the chain's end alone binds it (the board names a Project's principal, MD-3). A covered
306
325
  answer's `recordAs` names `caller`, the sender Project the board verified (MD-3 records the caller, as the action
307
326
  path does). None when no mandate travelled: the handler asks the operator as it would without one. The call's
308
327
  `spendUsd` and `project` are read failing closed, as the kit's other mandate readers read them (FAA-139): a spend
@@ -334,12 +353,54 @@ def covers(mandate: Optional[Dict[str, Any]], call: Any, agent_id: Optional[str]
334
353
  now = _format_rfc3339(clock * 1000) if math.isfinite(clock) else ""
335
354
  except (OverflowError, ValueError): # finite but outside the dates a clock can name
336
355
  now = ""
337
- answer = _mandate.mandate_covers({"mandate": mandate, "tier": "T1", "now": now, "self": agent_id, "call": call})
356
+ inputs = {"mandate": mandate, "tier": "T1", "now": now, "self": agent_id, "call": call}
357
+ if caller_agent is not None:
358
+ inputs["callerAgent"] = caller_agent
359
+ answer = _mandate.mandate_covers(inputs)
338
360
  if "recordAs" in answer:
339
361
  answer = {"ok": True, "recordAs": dict(answer["recordAs"], caller=caller)}
340
362
  return answer
341
363
 
342
364
 
365
+ def notice_links(links: Any) -> Dict[str, str]:
366
+ """A responder's `links` (FAA-187 (21)): the page on the service that resolves each notify kind, such as
367
+ `{"board.slot.moved": "/#board"}`, passed to a notice that notifies as its `link` (events-and-notifications.md).
368
+ A path on the service, as make_event takes one; None is none. A service that gives links has an emit that takes
369
+ `link` (JsonlEventLog.append does)."""
370
+ if links is None:
371
+ return {}
372
+ if not isinstance(links, Mapping) or not all(isinstance(k, str) and isinstance(v, str) and v.startswith("/")
373
+ and not v.startswith("//") for k, v in links.items()):
374
+ raise BoardError("invalid_arguments", "links maps an event kind to a path on this service, such as /#board.")
375
+ return dict(links)
376
+
377
+
378
+ def read_back_problem(message: Any, entry: Dict[str, Any], project: str) -> Optional[str]:
379
+ """Why a message read back with com.get is not the request a claim took (the first path refuses it, a resume reads
380
+ no mandate from it, FAA-196 (1)): `digest-mismatch` (another digest than the claim's), `not-addressed` (not this
381
+ request, capability and Project), else None."""
382
+ if not isinstance(message, dict) or message.get("digest") != entry["digest"]:
383
+ return "digest-mismatch"
384
+ req = message.get("request")
385
+ if not isinstance(req, dict) or req.get("id") != entry["request"] or req.get("capability") != entry["capability"] \
386
+ or req.get("target") != project:
387
+ return "not-addressed"
388
+ return None
389
+
390
+
391
+ def sender_agent(identify_agent: Optional[Callable[[Any], Optional[str]]], sender: Any, mandate: Any = True) -> Optional[str]:
392
+ """FAA-135: the agent id the service's identify_agent(sender) names for a request's sender (the board's verified
393
+ `{project, principal}`), read as the interop kit reads its own hook (`identified_agent`): None is none, an agent id
394
+ is that id, anything else, or a hook that raises, is an id no chain holds, so the mandate is refused. Asked only
395
+ when a mandate travelled (`mandate` not None)."""
396
+ if identify_agent is None or mandate is None:
397
+ return None
398
+ try:
399
+ return _rules.identified_agent(identify_agent(sender))
400
+ except Exception: # noqa: BLE001 - the service's own hook: fail closed
401
+ return _rules.UNREADABLE_AGENT
402
+
403
+
343
404
  def mandate_options(agent_id: Any, mandate_from: Any) -> Any:
344
405
  """The responder's opt-in, checked: an agent id the registry accepts, and `mandate_from` either `PERSONAL_BOARD` or
345
406
  a collection of Project ids (strings). Answers `mandate_from` as the responder keeps it (a frozenset or the string)."""
@@ -359,3 +420,31 @@ def mandate_options(agent_id: Any, mandate_from: Any) -> Any:
359
420
  def _fold(project: str) -> str:
360
421
  """A Project id as Fabric's board compares it: a uuid lower-cased, any other id as written (_hub._project)."""
361
422
  return project.lower() if _hub.UUID.fullmatch(project) else project
423
+
424
+
425
+ def notice_subject(value: Dict[str, Any], project: str, episode: Optional[str] = None) -> Dict[str, str]:
426
+ """A notice's subject (`{type, id}`): its request, else its capability, else its episode (a cause such as a block's
427
+ code, so two causes are two episodes for a host, seam r31), else the Project."""
428
+ for kind, given in (("request", value.get("request")), ("capability", value.get("capability")), ("episode", episode)):
429
+ if isinstance(given, str) and 0 < len(given) <= 128:
430
+ return {"type": kind, "id": given}
431
+ return {"type": "project", "id": project}
432
+
433
+
434
+ def takes_subject(emit: Callable[..., Any]) -> bool:
435
+ """Whether an emit's signature names a `subject` parameter, as JsonlEventLog.append does. Any other emit (one with
436
+ only **kwargs, which may forward them to a sink without it or set its own, or one whose signature cannot be read)
437
+ is called as 0.55.0 called it, without one (0.56.0 unit r33). A subject already bound into the emit, as with
438
+ functools.partial(log.append, subject=...), stands.
439
+
440
+ The signature is the emit's own, not a wrapped function's: a functools.wraps forwarder that sets its own subject
441
+ would otherwise be passed a second one (0.56.0 unit r34). Nothing here raises, so a notice never stops the
442
+ responder. A positional-only `subject` cannot take the keyword the responder passes, so it counts as none (FAA-187
443
+ (26)); the responder's own module imports both helpers under their old private names (moved here in 0.83.0).
444
+ """
445
+ try:
446
+ params = inspect.signature(emit, follow_wrapped=False).parameters.values()
447
+ return any(p.name == "subject" and p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
448
+ and (p.default is inspect.Parameter.empty or p.default is None) for p in params)
449
+ except Exception: # noqa: BLE001 - an unreadable signature (or a default that raises) is called without one
450
+ return False
@@ -1,9 +1,9 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """The board responder (DEC-0078, FAA-12b): the work context a handler sees and the responder loop (enroll, claim,
2
3
  accept the exact digest, effect, progress, complete; stop when fenced). The session, the checkpoint and the request live
3
4
  in fabric_board_link and are re-exported here. Part of the board connector (FAA-65, layer L4)."""
4
5
  from __future__ import annotations
5
6
 
6
- import inspect
7
7
  import json
8
8
  import random
9
9
  import re
@@ -28,6 +28,8 @@ from fabric_board_link import ( # noqa: E402,F401 - re-exported: callers import
28
28
  BoardSession,
29
29
  Checkpoint,
30
30
  PERSONAL_BOARD,
31
+ notice_subject as _notice_subject,
32
+ takes_subject as _takes_subject,
31
33
  )
32
34
  from fabric_board_slots import ( # noqa: E402,F401 - the slot records (FAA-155, FAA-163); the constants re-exported
33
35
  HOLD_RETRY_MAX_SECONDS,
@@ -51,6 +53,9 @@ IDLE_LEAVE_SECONDS = 3.0
51
53
 
52
54
  EVENT_SCHEMA = "urn:fabric:schema:board-responder-event:1"
53
55
 
56
+ #: `board.down` is said once per outage window: said again only after the board stayed up this long (FAA-187 (22)).
57
+ DOWN_QUIET_SECONDS = 300.0
58
+
54
59
  #: On these the attempt is no longer ours: stop acting at once (C4).
55
60
  STOP_CODES = frozenset(["fenced", "lease_expired", "authority_changed"])
56
61
 
@@ -60,8 +65,6 @@ TERMINAL = frozenset(["completed", "failed_known", "cancelled", "expired"])
60
65
  NOT_OURS = frozenset(["queued", "claimed", "answered"])
61
66
 
62
67
 
63
-
64
-
65
68
  class WorkContext:
66
69
  """What a handler may do while it works: `progress(note)` posts a note on the request's thread (and renews the
67
70
  lease); `begin_effect(summary)` must be called (and must return) before the handler spends or acts outside
@@ -70,12 +73,12 @@ class WorkContext:
70
73
  `mandate_covers(call)` whether it covers a call (`mandate_covers`' answer), None when none travelled."""
71
74
 
72
75
  def __init__(self, responder: "Responder", entry: Dict[str, Any], mandate: Optional[Dict[str, Any]] = None,
73
- caller: Optional[str] = None):
74
- self._responder, self._entry, self.mandate, self._caller = responder, entry, mandate, caller
76
+ caller: Optional[str] = None, caller_agent: Optional[str] = None):
77
+ self._responder, self._entry, self.mandate, self._caller, self._agent = responder, entry, mandate, caller, caller_agent
75
78
  self.stopped = threading.Event()
76
79
 
77
80
  def mandate_covers(self, call: Dict[str, Any]) -> Optional[Dict[str, Any]]:
78
- return _link.covers(self.mandate, call, self._responder.agent_id, self._responder.wall, self._caller)
81
+ return _link.covers(self.mandate, call, self._responder.agent_id, self._responder.wall, self._caller, self._agent)
79
82
 
80
83
  def progress(self, note: Optional[str] = None) -> None:
81
84
  if self.stopped.is_set():
@@ -100,33 +103,6 @@ class _Work:
100
103
  Handler = Callable[[BoardRequest, WorkContext], Dict[str, Any]]
101
104
 
102
105
 
103
-
104
- def _notice_subject(value: Dict[str, Any], project: str, episode: Optional[str] = None) -> Dict[str, str]:
105
- """A notice's subject (`{type, id}`): its request, else its capability, else its episode (a cause such as a block's
106
- code, so two causes are two episodes for a host, seam r31), else the Project."""
107
- for kind, given in (("request", value.get("request")), ("capability", value.get("capability")), ("episode", episode)):
108
- if isinstance(given, str) and 0 < len(given) <= 128:
109
- return {"type": kind, "id": given}
110
- return {"type": "project", "id": project}
111
-
112
-
113
- def _takes_subject(emit: Callable[..., Any]) -> bool:
114
- """Whether an emit's signature names a `subject` parameter, as JsonlEventLog.append does. Any other emit (one with
115
- only **kwargs, which may forward them to a sink without it or set its own, or one whose signature cannot be read)
116
- is called as 0.55.0 called it, without one (0.56.0 unit r33). A subject already bound into the emit, as with
117
- functools.partial(log.append, subject=...), stands.
118
-
119
- The signature is the emit's own, not a wrapped function's: a functools.wraps forwarder that sets its own subject
120
- would otherwise be passed a second one (0.56.0 unit r34). Nothing here raises, so a notice never stops the
121
- responder."""
122
- try:
123
- params = inspect.signature(emit, follow_wrapped=False).parameters.values()
124
- return any(p.name == "subject" and p.kind is not inspect.Parameter.VAR_KEYWORD
125
- and (p.default is inspect.Parameter.empty or p.default is None) for p in params)
126
- except Exception: # noqa: BLE001 - an unreadable signature (or a default that raises) is called without one
127
- return False
128
-
129
-
130
106
  class Responder(SlotKeeper):
131
107
  """One service's responder for one Project and one or more capabilities, one request at a time.
132
108
 
@@ -142,9 +118,11 @@ class Responder(SlotKeeper):
142
118
  emit: Optional[Callable[..., Any]] = None, service: str = "service",
143
119
  now: Callable[[], float] = time.monotonic, rand: Callable[[], float] = random.random,
144
120
  spawn: Optional[Callable[[Callable[[], None]], Any]] = None, session_factory: Optional[Callable[..., BoardSession]] = None,
145
- agent_id: Optional[str] = None, mandate_from: Any = None, wall: Callable[[], float] = time.time):
146
- if admit is None or handler is None:
147
- raise BoardError("invalid_arguments", "A responder needs the service's own input gate (admit) and a handler.")
121
+ agent_id: Optional[str] = None, mandate_from: Any = None, wall: Callable[[], float] = time.time,
122
+ identify_agent: Optional[Callable[[Dict[str, Any]], Optional[str]]] = None, links: Any = None):
123
+ if admit is None or handler is None or not (identify_agent is None or callable(identify_agent)):
124
+ raise BoardError("invalid_arguments", "A responder needs the service's own input gate (admit) and a handler; "
125
+ "identify_agent, when given, is a function of the sender (FAA-135).")
148
126
  caps = list(capabilities)
149
127
  if not caps or any(not isinstance(c, str) or not _CAPABILITY.fullmatch(c) for c in caps) or len(set(caps)) != len(caps):
150
128
  raise BoardError("invalid_arguments", "Name each capability once, [a-z][a-z0-9._-]{1,127}.")
@@ -152,8 +130,10 @@ class Responder(SlotKeeper):
152
130
  raise BoardError("invalid_arguments", "%s is a Project's primary responder's, a session's: a service answers "
153
131
  "the capabilities it declares (DEC-0140 PR-1)." % _fs.PRIMARY_CAPABILITY)
154
132
  self.hub, self.project = hub, _hub._project(project_id)
155
- #: FAA-135: read the operator's mandate from a trusted request of his own Project (opt-in; None reads none).
133
+ #: FAA-135: read the operator's mandate from a trusted request of his own Project (opt-in; None reads none), and
134
+ #: bind its chain with the agent id identify_agent names for the sender (asked only when a mandate travelled).
156
135
  self.mandate_from, self.agent_id, self.wall = _link.mandate_options(agent_id, mandate_from), agent_id, wall
136
+ self.identify_agent, self.links = identify_agent, _link.notice_links(links) # FAA-187 (21): a link per notify kind
157
137
  self.capabilities = caps
158
138
  self.credential_file, self.handler, self.admit, self.observe = Path(credential_file), handler, admit, observe
159
139
  self.checkpoint = Checkpoint(Path(checkpoint_file))
@@ -172,10 +152,12 @@ class Responder(SlotKeeper):
172
152
  self.claim_keys: Dict[str, str] = {}
173
153
  self._probing: set = set() # moved slots whose move-back claim lost its answer: asked again under the same key
174
154
  self._said: Dict[str, str] = {}
155
+ self._up_since: Optional[float] = None # when the board was last seen up after a fault (FAA-187 (22))
175
156
  self._rr = 0
176
157
  #: The first pause under an operator's hold (DEC-0079); it doubles to HOLD_RETRY_MAX_SECONDS.
177
158
  self.hold_delay = POLL_SECONDS
178
159
  self._on_hold: set = set()
160
+ self._taken_back: set = set() # capabilities the operator took back, not yet held again (FAA-325)
179
161
  #: One fenced call at a time, and the entry edits beside them (FAA-77, finding 36).
180
162
  self._lock = threading.RLock()
181
163
  self._to_resume: List[str] = [] # transferred requests waiting until nothing runs (finding 39)
@@ -203,6 +185,8 @@ class Responder(SlotKeeper):
203
185
  # goes once, without a subject there (unit r31, seam r31).
204
186
  if _takes_subject(self._emit_to):
205
187
  extra["subject"] = _notice_subject(value, self.project, episode)
188
+ if notify and kind in getattr(self, "links", {}): # the page that resolves it, which the service named (FAA-187 (21))
189
+ extra["link"] = self.links[kind]
206
190
  try:
207
191
  self._emit_to(kind, level, text, notify=notify, **extra)
208
192
  except Exception: # noqa: BLE001 - an event that cannot be written never stops the responder
@@ -267,8 +251,8 @@ class Responder(SlotKeeper):
267
251
  self.slots = {}
268
252
  self._recover(status, [c for c in self.capabilities if c in grant.capabilities])
269
253
  self.state = "ready"
270
- for kind in ("board.down", "board.blocked") + (() if missing else ("board.access.needed",)):
271
- self._said.pop(kind, None)
254
+ for kind in ("board.blocked",) + (() if missing else ("board.access.needed",)):
255
+ self._said.pop(kind, None) # board.down waits for its quiet window (step)
272
256
 
273
257
  def _enroll(self, capability: str) -> None:
274
258
  args = {"idempotency": self._idem(self._key("enroll")), "capability": capability}
@@ -525,6 +509,8 @@ class Responder(SlotKeeper):
525
509
  self.backoff.reset()
526
510
  if self.state == "down": # a lost call was sent again and answered: the session held throughout
527
511
  self.state = "ready"
512
+ self._up_since = now if self._up_since is None else self._up_since
513
+ if now - self._up_since >= DOWN_QUIET_SECONDS: # up a whole window: a later outage is said again
528
514
  self._said.pop("board.down", None)
529
515
  return self.interval()
530
516
  except (BoardError, _hub.HubError) as exc:
@@ -542,7 +528,7 @@ class Responder(SlotKeeper):
542
528
  return min(renewed) + LEASE_SECONDS - self.now() if renewed and self.session is not None else 0.0
543
529
 
544
530
  def _fault(self, exc: Exception) -> float:
545
- code, slots = getattr(exc, "code", "unreachable"), self.slots.values()
531
+ code, slots, self._up_since = getattr(exc, "code", "unreachable"), self.slots.values(), None
546
532
  connecting = self.state == "connecting" and not any(s.get("state") == "held" for s in slots)
547
533
  wait = self.backoff.next()
548
534
  left = self._lease_left() - LEASE_MARGIN_SECONDS
@@ -613,10 +599,11 @@ class Responder(SlotKeeper):
613
599
  for capability in list(self._suspect): # kept until com.status answered, so it is asked before any enroll
614
600
  self._gone(capability)
615
601
  self._suspect.discard(capability)
602
+ shared: Dict[str, Any] = {} # one com.status read a tick for every moved slot (FAA-194 (5))
616
603
  for capability, slot in list(self.slots.items()):
617
604
  if slot.get("state") == "moved": # never enrolled again on its own, even when the new holder lapses (FAA-155)
618
605
  if self.now() - slot.get("renewed_at", 0) >= RENEW_SECONDS - 0.5:
619
- self._moved_back(capability) # unless com.status shows a later generation it may hold (FAA-165)
606
+ self._moved_back(capability, shared) # unless com.status shows a later generation it may hold (FAA-165)
620
607
  continue
621
608
  if slot.get("state") == "paused":
622
609
  if capability != held_by_work and self.now() >= slot.get("retry_at", 0.0) and not self._gone(capability):
@@ -699,15 +686,14 @@ class Responder(SlotKeeper):
699
686
  "revision": receipt["request"]["revision"], "step": "claimed", "keys": {}, "effect": False}
700
687
  self.checkpoint.put(entry)
701
688
  message = self._call("com.get", {"message": entry["message"]})
702
- req = message.get("request") if isinstance(message, dict) else None
703
- if message.get("digest") != entry["digest"]:
689
+ problem = _link.read_back_problem(message, entry, self.project)
690
+ if problem == "digest-mismatch":
704
691
  self.checkpoint.drop(entry["request"])
705
692
  self._emit("board.request.skipped", "warning", "A claimed request was not accepted: the message read back carries "
706
693
  "another digest than the claim. Nothing was done; the claim lapses.", request=entry["request"],
707
694
  capability=capability, reason="digest-mismatch")
708
695
  return
709
- if not isinstance(req, dict) or req.get("id") != entry["request"] or req.get("capability") != capability \
710
- or req.get("target") != self.project:
696
+ if problem:
711
697
  self.checkpoint.drop(entry["request"])
712
698
  self._emit("board.request.skipped", "warning", "A claimed request was not accepted: it is not addressed to this "
713
699
  "Project and capability. Nothing was done; the claim lapses.", request=entry["request"],
@@ -754,12 +740,15 @@ class Responder(SlotKeeper):
754
740
  self.checkpoint.put(entry)
755
741
  return True
756
742
 
757
- def _run(self, entry: Dict[str, Any], request: BoardRequest) -> None:
743
+ def _run(self, entry: Dict[str, Any], request: BoardRequest, problem: Optional[str] = None) -> None:
758
744
  mandate, unread = _link.read_mandate(request, self.mandate_from, self.agent_id)
745
+ if problem and (mandate is not None or unread): # FAA-196 (1): a resume read back another message than accepted
746
+ mandate, unread = None, problem
759
747
  if unread: # by its code alone: the block holds the operator's words
760
748
  self._emit("board.request.mandate_unread", "warning", "Request %s carries a mandate the kit does not read (%s); "
761
749
  "it is worked without one." % (entry["request"], unread), request=entry["request"], reason=unread)
762
- ctx = WorkContext(self, entry, mandate, request.sender.get("project") if isinstance(request.sender, dict) else None)
750
+ sender = request.sender if isinstance(request.sender, dict) else {}
751
+ ctx = WorkContext(self, entry, mandate, sender.get("project"), _link.sender_agent(self.identify_agent, sender, mandate))
763
752
  work = _Work(entry, request, ctx)
764
753
  work.renewed_at = self.now()
765
754
  self.current = work
@@ -781,7 +770,7 @@ class Responder(SlotKeeper):
781
770
  self._complete(entry)
782
771
  return
783
772
  message = self._call("com.get", {"message": entry["message"]})
784
- self._run(entry, BoardRequest(message, entry["capability"]))
773
+ self._run(entry, BoardRequest(message, entry["capability"]), _link.read_back_problem(message, entry, self.project))
785
774
 
786
775
  def _fenced(self, op: str, entry: Dict[str, Any], key: str, extra: Optional[Dict[str, Any]] = None,
787
776
  apply: Optional[Callable[[Dict[str, Any]], None]] = None) -> Optional[Dict[str, Any]]:
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """The project board's wire (fabric-project-comms/0.1, DEC-0022, DEC-0073, DEC-0077): constants, the refusal codes, the
2
3
  contract's request digest, the closed shapes the kit reads back (receipts, refusals, directory pages, messages) and
3
4
  evidence. Part of the board connector (FAA-65, layer L4)."""
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """The board responder's slot records (FAA-155, FAA-163): the generation it holds each slot at, a move to another
2
3
  session and its take-back, and a capability whose enroll was refused, retried on its own. Part of the board connector
3
4
  (FAA-65, layer L4); `Responder` (fabric_board_session) is a `SlotKeeper`."""
@@ -35,6 +36,10 @@ class SlotKeeper:
35
36
  self._confirmed[capability] = generation
36
37
  self._said.pop("board.slot.refused:" + capability, None) # held again: a later refusal is said again
37
38
  self._said.pop("board.slot.contested:" + capability, None) # and a later contest, by enroll or replace
39
+ if capability in self._taken_back: # FAA-325: held again after the operator's take-back, said once
40
+ self._taken_back.discard(capability)
41
+ self._emit("board.slot.returned", "info", "The %s slot on this Project is this service's again: the operator "
42
+ "took it back, and it answers it from now on." % capability, capability=capability, generation=generation)
38
43
 
39
44
  def _note(self, capability: str, record: Optional[Dict[str, Any]]) -> None:
40
45
  """The slot record, best effort: one that cannot be written now is written at the next receipt."""
@@ -125,19 +130,20 @@ class SlotKeeper:
125
130
  self._note(capability, dict(held, moved=now)) # said again at the next start if it cannot be written
126
131
  if "moved" not in held: # once, even across restarts
127
132
  self._emit("board.slot.moved", "warning", "The %s slot on this Project moved to another session. This service stopped answering it; accepted "
128
- "work stays `accepted` (work begun is the new holder's). The operator brings it back by take-back (once the "
129
- "other session lets it go, or at a restart with accepted work waiting; a restart alone does not) or, where "
130
- "Fabric offers it, a move back here. Else ask its maintainer." % capability,
133
+ "work stays accepted (work begun is the new holder's). The operator brings it back by take-back (once the "
134
+ "other session lets it go, or at a restart or reconnect with accepted work waiting; neither alone does) or, "
135
+ "where Fabric can, a move back here. Else ask its maintainer." % capability,
131
136
  notify=True, episode=capability, capability=capability, generation=now, held=held["generation"])
132
137
  return True
133
138
 
134
- def _moved_back(self, capability: str) -> None:
139
+ def _moved_back(self, capability: str, shared: Optional[Dict[str, Any]] = None) -> None:
135
140
  """A moved slot asked about once per later generation (FAA-165): the operator may move it back to this session
136
141
  (DEC-0140 PR-2), which this client cannot hear, so when com.status shows a generation past the recorded move, one
137
142
  `com.claim` asks whether the slot is its own. A claim that lands makes it held again and starts what it handed
138
143
  out; a `lease_expired` one (the move reached this session but its lease lapsed) enrolls again; a fenced one is
139
144
  another session's, recorded so the same generation is not asked about twice. Never an enroll or a replace on a
140
- slot another session holds: a slot moved off stays off unless it is handed back."""
145
+ slot another session holds: a slot moved off stays off unless it is handed back. `shared` holds the tick's one
146
+ com.status read, shared by every moved slot (FAA-194 (5))."""
141
147
  if self.current is not None or self.stopping:
142
148
  return # a claim now could hand out a second request, or start work while the responder stops
143
149
  held = self.checkpoint.slots.get(capability) or {}
@@ -145,7 +151,12 @@ class SlotKeeper:
145
151
  if held.get("project") != self.project or not _is_int(seen, 1):
146
152
  return
147
153
  (self.slots.get(capability) or {})["renewed_at"] = self.now()
148
- status = self._status()
154
+ if shared is not None and "status" in shared:
155
+ status = shared["status"]
156
+ else:
157
+ status = self._status()
158
+ if shared is not None:
159
+ shared["status"] = status
149
160
  now = _whole(next((r.get("generation") for r in status.get("responders") or [] if isinstance(r, dict) and r.get("capability") == capability), None))
150
161
  if not _is_int(now, 1) or now <= seen:
151
162
  return
@@ -200,9 +211,12 @@ class SlotKeeper:
200
211
  if self.checkpoint.slots.get(capability) != held:
201
212
  return False # take_back (an action thread) forgot the record meanwhile; its own next tick enrolls
202
213
  self.slots[capability] = {"state": "held", "generation": generation, "renewed_at": float("-inf")} # no lease yet
214
+ self._taken_back.discard(capability) # said below as a move back, once
203
215
  self._held_at(capability, generation)
204
216
  self._said.pop("board.slot.moved", None) # a later move is said again
205
- return True
217
+ self._emit("board.slot.returned", "info", "The %s slot on this Project is this service's again: the operator moved it "
218
+ "back here, and it answers it from now on." % capability, capability=capability, generation=generation)
219
+ return True # FAA-194 (8): the feed no longer ends on the move's warning
206
220
 
207
221
  def take_back(self, capability: str) -> None:
208
222
  """The operator's act, never the kit's: forget the move; the next tick enrolls once the slot is free (FAA-155)."""
@@ -212,9 +226,12 @@ class SlotKeeper:
212
226
  self.checkpoint.slot(capability, None)
213
227
  self.claim_keys.pop(capability, None) # a receipt from before the take-back may be dead
214
228
  self._probing.discard(capability)
215
- if (self.slots.get(capability) or {}).get("state") == "moved":
229
+ state = (self.slots.get(capability) or {}).get("state")
230
+ if state == "moved":
216
231
  self.slots[capability] = {"state": "contested", "renewed_at": 0.0}
217
232
  self._said.pop("board.slot.moved", None)
233
+ if state != "held": # board.slot.returned is said once the slot is held again (FAA-325); held, nothing to say
234
+ self._taken_back.add(capability)
218
235
 
219
236
  # -- one capability's refusal (FAA-163) ------------------------------------------------------------------------
220
237
  def _retry_later(self, capability: str, entries: List[Dict[str, Any]], exc: BoardError) -> None:
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
2
3
  """The estate ceremonies' headless join (contract DEC-0084), the Python kit; the Node twin is fabric-ceremonies.mjs.
3
4
  Standard library only.
4
5
 
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """What an agent uses: `fabric-connections/0.1`, the extension keyed extensions/connections/0.1 (DEC-0096, FAC-SEM-127).
2
3
  `connections_view` reads a well-known document as the contract's `connectionsView` does (it never raises);
3
4
  `connections_extension` builds the `extensions` entry a service declares, `state` from its own checks and its
@@ -6,7 +7,7 @@ from __future__ import annotations
6
7
 
7
8
  import re
8
9
  import urllib.parse
9
- from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence
10
+ from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
10
11
 
11
12
  import sys as _sys # noqa: E402
12
13
  from pathlib import Path as _Path # noqa: E402
@@ -52,7 +53,8 @@ _KEY_SHAPE = re.compile(
52
53
  r"(?:^|[^A-Za-z0-9])(?:sk-(?=[A-Za-z0-9_-]*[0-9])(?:(?=[A-Za-z0-9_-]*[A-Z])[A-Za-z0-9_-]{16,}|[A-Za-z0-9_-]{32,})"
53
54
  r"|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,}"
54
55
  r"|(?:bot)?[0-9]{6,12}:(?=[A-Za-z0-9_-]*[A-Z])(?=[A-Za-z0-9_-]*[a-z])[A-Za-z0-9_-]{34,35}(?![A-Za-z0-9_-]))")
55
- _URL_SCHEME = re.compile(r"[a-z][a-z0-9+.-]*://", re.IGNORECASE | re.ASCII)
56
+ _SCHEME_LETTERS = frozenset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
57
+ _SCHEME_CHARS = _SCHEME_LETTERS | frozenset("0123456789+.-")
56
58
  _CLOSING = ")]}>\"'.,;:!?"
57
59
  _TRIED_BACK = 1
58
60
 
@@ -139,13 +141,33 @@ def _shape_problems(value: Any) -> List[str]:
139
141
  return problems
140
142
 
141
143
 
144
+ def _scheme_spans(piece: str) -> List[Tuple[int, int]]:
145
+ """The (start, end) of each URL scheme and its `://` in a piece, where `[a-z][a-z0-9+.-]*://` (case-insensitive,
146
+ ASCII) finds them left to right, read in linear time (FAA-322: the pattern tried every letter of a long run with no
147
+ `://`). From each `://` the run of scheme characters before it is read back, and the match starts at its first
148
+ letter at or after where the last match ended."""
149
+ spans: List[Tuple[int, int]] = []
150
+ after, at = 0, piece.find("://")
151
+ while at >= 0:
152
+ start = at
153
+ while start > after and piece[start - 1] in _SCHEME_CHARS:
154
+ start -= 1
155
+ while start < at and piece[start] not in _SCHEME_LETTERS:
156
+ start += 1
157
+ if start < at:
158
+ spans.append((start, at + 3))
159
+ after = at + 3
160
+ at = piece.find("://", at + 1)
161
+ return spans
162
+
163
+
142
164
  def _urls_in(value: str) -> List[str]:
143
165
  """The URLs a member may hold, from every scheme in each whitespace-delimited piece, without the closing brackets,
144
166
  quotes and punctuation that end a sentence, and with _TRIED_BACK of them put back (`]` closes an IPv6 host)."""
145
167
  urls: List[str] = []
146
168
  for piece in _SPACES.split(value):
147
- for match in _URL_SCHEME.finditer(piece):
148
- url = piece[match.start():]
169
+ for start, _end in _scheme_spans(piece):
170
+ url = piece[start:]
149
171
  end = len(url)
150
172
  while end > 0 and url[end - 1] in _CLOSING:
151
173
  end -= 1
@@ -160,8 +182,8 @@ def _long_run(value: str) -> bool:
160
182
  """A long secret-shaped run in a piece read whole; in a piece that holds a URL, the text before its first scheme's
161
183
  `://` (the URL itself is read by secret_url)."""
162
184
  for piece in _SPACES.split(value):
163
- found = _URL_SCHEME.search(piece)
164
- cut = piece.find("://", found.start()) if found else -1
185
+ found = _scheme_spans(piece)
186
+ cut = found[0][1] - 3 if found else -1
165
187
  if _long_secret(piece if cut < 0 else piece[:cut]):
166
188
  return True
167
189
  return False
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """One instance per context (FAA-125, organization row N-11; contract DEC-0082, "One instance per context on a machine"):
2
3
  from a context, ``personal`` or ``org:<orgId>``, the service instance, its folders, descriptor and instance lock. Part of
3
4
  the service kit (layer L3, after fabric_descriptor); the twin of fabric-context.mjs. Standard library only.
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """The kit's cryptography, once (FAA-65, layer L1): what the contract's verifiers need and nothing more.
2
3
 
3
4
  - ``ed25519_verify(public_key, message, signature)`` (RFC 8032) and ``is_small_order_key`` with