@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,4 +1,5 @@
1
1
  #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
2
3
  """The callee's side of fabric-delegation/0.1 (Fabric Agent Contract DEC-0039, Fabric ADR-0144): check a
3
4
  delegation token or an approval offline against the estate's published keys, and answer `ok` or one refusal
4
5
  code. A port of the contract's `src/delegation.ts` at the pinned revision; the shared vectors in
@@ -46,6 +47,8 @@ from fabric_jsvalue import ( # noqa: E402 - the core (FAA-65)
46
47
  )
47
48
  from fabric_paths import atomic_write as _atomic_write # noqa: E402 - the kit's private writer (FAA-85)
48
49
  import fabric_peer_owner as _po # noqa: E402 - a loopback key set is read from this account's listener only (FAA-255)
50
+ import fabric_secrets # noqa: E402 - the WHATWG host reading (FAA-98, FAA-309)
51
+ import fabric_socket_proof # noqa: E402 - the one loopback host rule (FAA-187 (15))
49
52
  from fabric_mandate import effect_closure # noqa: E402,F401 - defined once beside the mandate that reads it too (DEC-0125)
50
53
 
51
54
  REFUSAL_CODES = (
@@ -621,16 +624,10 @@ def action_digest(action: Any) -> str:
621
624
 
622
625
  # --- the estate's keys ----------------------------------------------------------------------------------------
623
626
 
624
- def _loopback(host: str) -> bool:
625
- if host == "localhost":
626
- return True
627
- try:
628
- address = ipaddress.ip_address(host)
629
- except ValueError:
630
- return False
631
- # A zone (`::1%eth0`) is no loopback: urllib decodes `%3A80` in it into another address (0.56.0 seam r13).
632
- # Nor an IPv4-mapped address, which Python 3.13+ counts as loopback and 3.9 and Node do not (0.56.0 seam r14).
633
- return address.is_loopback and not getattr(address, "scope_id", None) and not getattr(address, "ipv4_mapped", None)
627
+ #: A key set source's loopback host, the kit's one rule (fabric_socket_proof.loopback_host), shared with fabric_http's
628
+ #: check_url, so a source starts a service on both kits alike (FAA-187 (15); `127.1`, `2130706433`, a name under
629
+ #: `.localhost`). A zone and an IPv4-mapped address stay refused, as before (0.56.0 seam r13, r14).
630
+ _loopback = fabric_socket_proof.loopback_host
634
631
 
635
632
 
636
633
  # fabric_http._URL_ENDS (this module may not import fabric_http; test_fabric_delegation pins them equal).
@@ -663,7 +660,16 @@ def _source_authority_ok(parts: urllib.parse.SplitResult) -> bool:
663
660
  parts.port # noqa: B018 - raises on a port above 65535
664
661
  except ValueError:
665
662
  return False
666
- return bool(parts.hostname)
663
+ # A host `new URL` refuses for its structure (`a127.0.0.1`, `09`), or one Python's IDNA codec cannot write for
664
+ # http.client (U+180E), is refused here as check_url refuses it, never read and failed on every call (FAA-187 (14),
665
+ # (16)).
666
+ host = parts.hostname or ""
667
+ try:
668
+ if not host.isascii():
669
+ host.encode("idna")
670
+ except UnicodeError:
671
+ return False
672
+ return bool(host) and not fabric_secrets.url_refused(parts.geturl())
667
673
 
668
674
 
669
675
  class _NoRedirect(urllib.request.HTTPRedirectHandler):
@@ -734,7 +740,7 @@ def _fetch(source: str) -> Any:
734
740
  body += chunk
735
741
  if deadline["fired"] or time.monotonic() > deadline["end"]:
736
742
  raise TimeoutError("the key set did not arrive within %s s" % JWKS_FETCH_SECONDS)
737
- return json.loads(body[:65536])
743
+ return json.loads(body[:65536], parse_constant=_no_constant) # NaN is no JSON, as JSON.parse reads it (FAA-194)
738
744
  except urllib.error.HTTPError as exc:
739
745
  exc.close()
740
746
  raise
@@ -745,7 +751,7 @@ def _fetch(source: str) -> Any:
745
751
  finally:
746
752
  if deadline["timer"] is not None:
747
753
  deadline["timer"].cancel()
748
- return json.loads(Path(source).expanduser().read_text())
754
+ return json.loads(Path(source).expanduser().read_text(), parse_constant=_no_constant)
749
755
 
750
756
 
751
757
  class JwksCache:
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """How a service is found (fabric-service/0.1): its descriptor, peers and app descriptors (DEC-0061, DEC-0069),
2
3
  build identity and the well-known document, the loopback HTTP server. Part of the service kit (FAA-65, layer L3)."""
3
4
  # Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
@@ -55,7 +56,7 @@ from fabric_usage import (
55
56
  DASHBOARD_EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1"
56
57
 
57
58
 
58
- DASHBOARD_KIT = "fabric-dashboard-kit@1.1.7"
59
+ DASHBOARD_KIT = "fabric-dashboard-kit@1.1.9"
59
60
 
60
61
 
61
62
  REFERENCE_THEMES = ("graphite", "fabric", "paper", "forest", "ocean", "ember", "contrast")
@@ -197,9 +198,11 @@ def read_descriptors(directory: Optional[Path] = None) -> List[Tuple[Path, Dict[
197
198
  return found
198
199
  for path in sorted(root.glob("*.json")):
199
200
  try:
200
- found.append((path, json.loads(path.read_text(encoding="utf-8"))))
201
- except (OSError, ValueError):
201
+ value = json.loads(path.read_text(encoding="utf-8"))
202
+ except (OSError, ValueError, RecursionError):
202
203
  continue
204
+ if isinstance(value, dict): # a file that holds no object is no descriptor (FAA-200 (50))
205
+ found.append((path, value))
203
206
  return found
204
207
 
205
208
 
@@ -222,10 +225,11 @@ def validate_descriptor(descriptor: Dict[str, Any]) -> List[str]:
222
225
  return problems
223
226
  if descriptor["protocol"] != PROTOCOL:
224
227
  problems.append("protocol must be %s" % PROTOCOL)
225
- if not _ID.match(str(descriptor["id"])):
226
- problems.append("id must match %s" % _ID.pattern)
227
- if not _INSTANCE.match(str(descriptor["instance"])):
228
- problems.append("instance must match %s" % _INSTANCE.pattern)
228
+ for key, pattern in (("id", _ID), ("instance", _INSTANCE)): # a string before its pattern (FAA-200 (9), (36))
229
+ if not isinstance(descriptor[key], str):
230
+ problems.append("%s must be a string" % key)
231
+ elif not pattern.match(descriptor[key]):
232
+ problems.append("%s must match %s" % (key, pattern.pattern))
229
233
  if remote:
230
234
  problems.extend(remote_origin_problems(descriptor["origin"]))
231
235
  elif "origin" in descriptor and not _ORIGIN.match(str(descriptor["origin"])):
@@ -234,11 +238,11 @@ def validate_descriptor(descriptor: Dict[str, Any]) -> List[str]:
234
238
  if "tokenFile" not in auth:
235
239
  problems.append("auth.tokenFile is required")
236
240
  elif not _LOCAL_PATH.match(str(auth["tokenFile"])):
237
- problems.append("auth.tokenFile must be a local path, never a network share")
241
+ problems.append("auth.tokenFile must be an absolute local path (/, ~/ or a drive letter), never a relative path, never a network share")
238
242
  paths = descriptor.get("paths") or {}
239
243
  for value in ([paths["data"]] if "data" in paths else []) + list(paths.get("logs") or []):
240
244
  if not _LOCAL_PATH.match(str(value)):
241
- problems.append("paths must be local paths, never a network share: %r" % value)
245
+ problems.append("paths must be absolute local paths (/, ~/ or a drive letter), never a relative path, never a network share: %r" % value)
242
246
  header = auth.get("header", "Authorization")
243
247
  scheme = auth.get("scheme", "Bearer")
244
248
  if not (isinstance(header, str) and _AUTH_HEADER.fullmatch(header)):
@@ -405,7 +409,7 @@ def descriptor_member_problems(descriptor: Dict[str, Any]) -> List[str]:
405
409
  elif "@" in repository and not _HIERARCHICAL.match(repository): # 0.62.0 readings 7: never an scp form's user
406
410
  problems.append("source.repository with a user is a URL with an authority (https://user@host/path), never an scp form")
407
411
  elif fabric_secrets.repository_credential(repository): # its name is no key (0.62.0 seam reading 6)
408
- problems.append("FAC-SEM-086: /source/repository holds a value shaped like a credential; it names a repository")
412
+ problems.append("FAC-SEM-086: /source/repository holds a value shaped like a credential; it names a repository: write the repository's name in lower case if it is one")
409
413
  if "peer" in descriptor:
410
414
  peer = descriptor.get("peer")
411
415
  if not isinstance(peer, dict):
@@ -589,8 +593,15 @@ def port_listener_refusal(port: int, reader: Optional[Any] = None) -> Optional[s
589
593
  "FAA-225)." % (port, uid))
590
594
 
591
595
 
596
+ def warn_no_source(descriptor: Dict[str, Any], stacklevel: int = 2) -> None:
597
+ """The rewrite warning of a descriptor that names no source.repository (FAA-198)."""
598
+ me = "%s.%s" % (descriptor["id"], descriptor["instance"])
599
+ warnings.warn("Descriptor %s %s; a new install is refused without it." % (me, NO_SOURCE_REPOSITORY),
600
+ NoSourceRepositoryWarning, stacklevel=stacklevel + 1)
601
+
602
+
592
603
  def write_descriptor(descriptor: Dict[str, Any], directory: Optional[Path] = None, *, _stacklevel: int = 2,
593
- port_owner: Optional[Any] = None) -> Path:
604
+ port_owner: Optional[Any] = None, _warned: bool = False) -> Path:
594
605
  """Installer-only. Refuses a port or id.instance another descriptor claims (a port is a claim), and a port another
595
606
  OS account listens on (FAA-225; ``port_owner(port) -> (kind, uid) | None`` reads it, the kit's own by default).
596
607
 
@@ -604,9 +615,8 @@ def write_descriptor(descriptor: Dict[str, Any], directory: Optional[Path] = Non
604
615
  refusal = source_refusal(descriptor, root)
605
616
  if refusal:
606
617
  raise ServiceError(refusal)
607
- if not names_repository(descriptor):
608
- warnings.warn("Descriptor %s %s; a new install is refused without it." % (me, NO_SOURCE_REPOSITORY),
609
- NoSourceRepositoryWarning, stacklevel=_stacklevel)
618
+ if not names_repository(descriptor) and not _warned: # register_remote warns itself, before its token (FAA-200 (1))
619
+ warn_no_source(descriptor, _stacklevel)
610
620
  # DEC-0019: only a local placement claims a port on this computer; DEC-0069: an app's descriptor names none.
611
621
  port = None if placement_of(descriptor) == "remote" or "origin" not in descriptor else port_of(descriptor["origin"])
612
622
  for path, other in read_descriptors(root):
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
2
3
  """`estate-record/1` (Fabric Agent Contract DEC-0055): verify an estate revision and check its rules.
3
4
 
4
5
  One revision of one person's estate: the passkeys that sign it, the machines with their device keys and hub-key
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
2
3
  """`estate-record/1` (Fabric Agent Contract DEC-0055): the record's own rules, without a verifier.
3
4
 
4
5
  The contract's `src/estate-record-rules.ts` at the pin, ported answer for answer and held to the contract's own vectors
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """A service's events (DEC-0029): making them, paging them and the JSONL event log. Part of the service kit
2
3
  (FAA-65, layer L3)."""
3
4
  from __future__ import annotations
@@ -51,7 +52,9 @@ def make_event(event_id: Any, at: str, kind: str, level: str, text: str, *, subj
51
52
  link: Optional[str] = None, notify: bool = False, trace_id: Optional[str] = None,
52
53
  span_id: Optional[str] = None, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
53
54
  """One activity event: one sentence a person reads, never a machine id. `level` is one of `LEVELS`; `warn`, the
54
- dashboard chip's name, is stored as `warning`. `data` (DEC-0070) adds machine-readable
55
+ dashboard kit's chip for "a person is needed" (`.fd-s-warn`), is stored as `warning`, the feed's name for the same
56
+ level, so a reader that draws chips shows a stored `warning` as the `warn` chip (FAA-276, FAA-277 (6)). `data`
57
+ (DEC-0070) adds machine-readable
55
58
  fields beside the sentence, `{schema, value}`, never a secret: it is refused here, so no feed carries one.
56
59
 
57
60
  An event about traced work carries its trace as a pair, `trace_id` and `span_id`
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """Work a service holds by its own limits, said to its caller and its operator (FAA-298). Part of the service kit
2
3
  (layer L3, after fabric_interop).
3
4
 
@@ -21,7 +22,8 @@ says so three ways:
21
22
 
22
23
  `HoldBook` keeps one JSON file (0600, written atomically); a ThreadingHTTPServer's handlers share it under the store
23
24
  lock. It holds caller names, work labels, reasons and the operator's words, never a credential: `hold` refuses a text
24
- the contract's detector reads as one.
25
+ the contract's detector reads as one. A record written since 0.83.0 carries `squash: "ecmascript"`; one without it
26
+ (0.81.0, 0.82.0) is matched under both squashes, since 0.81.0's Python twin squashed names with str.split() (FAA-320).
25
27
  """
26
28
  from __future__ import annotations
27
29
 
@@ -65,6 +67,27 @@ def _squash(text: str) -> str:
65
67
  return _SPACES.sub(" ", text).strip(" ")
66
68
 
67
69
 
70
+ #: What a record written since 0.83.0 carries: its names were squashed with ECMAScript's whitespace (FAA-320).
71
+ SQUASH = "ecmascript"
72
+ #: Python's str.isspace() class, the one 0.81.0's Python twin squashed a name with (`" ".join(text.split())`).
73
+ _PY_SPACES = re.compile("[\t\n\x0b\x0c\r\x1c-\x1f \x85\xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+")
74
+
75
+
76
+ def _legacy_squash(text: str) -> str:
77
+ """A name as 0.81.0's Python twin wrote it (str.split()), written out so both twins read it alike."""
78
+ return _PY_SPACES.sub(" ", text).strip(" ")
79
+
80
+
81
+ def _names(caller: Any, record: Dict[str, Any]) -> tuple:
82
+ """The stored forms `caller` matches in `record`: the ECMAScript squash, and for a record with no `squash` mark
83
+ (written before 0.83.0) also the squash 0.81.0's Python twin wrote (FAA-320). None matches only None."""
84
+ if not isinstance(caller, str):
85
+ return (caller,)
86
+ if record.get("squash") == SQUASH:
87
+ return (_squash(caller),)
88
+ return (_squash(caller), _legacy_squash(caller))
89
+
90
+
68
91
  def _instant(value: Any, name: str) -> str:
69
92
  if utc_time(value) is None:
70
93
  raise ServiceError("%s is a UTC instant (Z, at most 3 fraction digits)." % name)
@@ -197,6 +220,7 @@ class HoldBook:
197
220
  job_id: Optional[str] = None, now: Optional[str] = None) -> Dict[str, Any]:
198
221
  """Hold `work` for `caller` by `reason`. Returns {hold, statusMessage, push}: `push` is the notify event when this
199
222
  call opened the hold, None when the work joined an open one (one push per hold)."""
223
+ raw_caller = caller
200
224
  caller, work = _text(caller, "caller"), _text(work, "work")
201
225
  if reason not in HOLD_REASONS:
202
226
  raise ServiceError("A hold's reason is one of %s." % ", ".join(HOLD_REASONS))
@@ -207,11 +231,11 @@ class HoldBook:
207
231
  data = self._load()
208
232
  self._expire(data, utc_time(at))
209
233
  hold = next((h for h in data["holds"] if h.get("closed") is None and h["reason"] == reason
210
- and h["caller"] == caller), None)
234
+ and h["caller"] in _names(raw_caller, h)), None)
211
235
  opened = hold is None
212
236
  if opened:
213
237
  hold = {"id": uuid.uuid4().hex, "reason": reason, "caller": caller, "work": work, "since": at,
214
- "until": until, "jobs": [], "closed": None}
238
+ "until": until, "jobs": [], "closed": None, "squash": SQUASH}
215
239
  data["holds"].append(hold)
216
240
  if job_id is not None and job_id not in hold["jobs"]:
217
241
  hold["jobs"].append(str(job_id))
@@ -229,7 +253,7 @@ class HoldBook:
229
253
  with self._lock:
230
254
  data = self._load()
231
255
  hold = next((h for h in data["holds"] if h.get("closed") is None and h["reason"] == reason
232
- and h["caller"] == caller), None)
256
+ and h["caller"] in _names(caller, h)), None)
233
257
  if hold is None:
234
258
  return None
235
259
  hold["closed"], hold["closedAt"] = "released", at
@@ -269,14 +293,15 @@ class HoldBook:
269
293
  raise ServiceError("A raised limit needs its window's end (until): it falls back when the window ends.")
270
294
  record: Dict[str, Any] = {"id": uuid.uuid4().hex, "reason": reason, "caller": None if caller is None else _text(caller, "caller"),
271
295
  "job": None if job_id is None else str(job_id), "from": at, "until": until,
272
- given[0]: size, "decision": dict(decision), "at": at}
296
+ given[0]: size, "decision": dict(decision), "at": at, "squash": SQUASH}
273
297
  if given[0] != "limit":
274
298
  record["left"] = size
275
299
  with self._lock:
276
300
  data = self._load()
277
301
  self._expire(data, utc_time(at))
278
302
  for hold in data["holds"]:
279
- if hold.get("closed") is None and hold["reason"] == reason and record["caller"] in (None, hold["caller"]) and (
303
+ if hold.get("closed") is None and hold["reason"] == reason and (
304
+ caller is None or hold["caller"] in _names(caller, hold)) and (
280
305
  job_id is None or str(job_id) in hold["jobs"]):
281
306
  hold["closed"], hold["closedAt"], hold["lift"] = "lifted", at, record["id"]
282
307
  data["lifts"].append(record)
@@ -291,7 +316,7 @@ class HoldBook:
291
316
  def _matching(self, data: Dict[str, Any], reason: str, caller: Optional[str], job_id: Optional[str], at: int,
292
317
  kind: str) -> List[Dict[str, Any]]:
293
318
  return [l for l in data["lifts"] if l["reason"] == reason and kind in l and self._live(l, at)
294
- and l.get("caller") in (None, caller) and l.get("job") in (None, job_id)]
319
+ and (l.get("caller") is None or l["caller"] in _names(caller, l)) and l.get("job") in (None, job_id)]
295
320
 
296
321
  def admit(self, *, reason: str, caller: str, job_id: Optional[str] = None, cost_usd: Optional[float] = None,
297
322
  now: Optional[str] = None) -> Dict[str, Any]:
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """An outbound HTTP client for a service: one deadline across every attempt, bounded retries with jittered backoff,
2
3
  `Retry-After` honoured up to a cap, a response size cap, and no secret in an error.
3
4
 
@@ -50,6 +51,8 @@ from pathlib import Path as _Path # noqa: E402
50
51
  if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from any working directory (FAA-65)
51
52
  _sys.path.insert(0, str(_Path(__file__).resolve().parent))
52
53
  from fabric_jsvalue import json_stringify # noqa: E402 - beside this file
54
+ import fabric_secrets # noqa: E402 - beside this file: the WHATWG host reading (FAA-98, FAA-309)
55
+ import fabric_socket_proof # noqa: E402 - beside this file: the one loopback host rule (FAA-187 (15))
53
56
  import fabric_peer_owner as _po # noqa: E402 - beside this file: the listener's owner (DEC-0154 HA-2)
54
57
 
55
58
  RETRY_STATUSES = (408, 425, 429, 500, 502, 503, 504)
@@ -211,28 +214,28 @@ def redact_url(url: str) -> str:
211
214
  return urllib.parse.urlunsplit((parts.scheme.lower(), netloc, path, "", ""))
212
215
 
213
216
 
214
- def _is_loopback(host: str) -> bool:
215
- """Loopback as a WHATWG URL parser leaves the host: one trailing dot dropped from an IPv4 address, no whitespace,
216
- and an IPv4-mapped IPv6 address not counted (Node writes it in hex and the Node twin refuses it)."""
217
- if host == "localhost" or host.endswith(".localhost"):
217
+ #: Loopback as a WHATWG URL parser leaves the host: the kit's one rule, shared with fabric_delegation (FAA-187 (15)).
218
+ _is_loopback = fabric_socket_proof.loopback_host
219
+
220
+
221
+ def _idna_sendable(host: Optional[str]) -> bool:
222
+ """Whether http.client can write the host: a non-ASCII one goes out in IDNA through Python's own codec, which refuses
223
+ some hosts `new URL` maps (U+180E, a fullwidth dot beside a dot), so such a URL would fail on every call (FAA-187
224
+ (16)); it is refused at the check instead."""
225
+ if not host or host.isascii():
218
226
  return True
219
- if not host or any(c.isspace() for c in host):
220
- return False
221
227
  try:
222
- address = ipaddress.ip_address(host)
223
- return address.is_loopback and not getattr(address, "ipv4_mapped", None)
224
- except ValueError:
225
- pass
226
- host = host[:-1] if host.endswith(".") else host
227
- try: # the short IPv4 forms a WHATWG URL also reads (127.1, 2130706433, 0x7f.1)
228
- return socket.inet_aton(host)[0] == 127
229
- except (OSError, ValueError):
228
+ host.encode("idna")
229
+ return True
230
+ except UnicodeError:
230
231
  return False
231
232
 
232
233
 
233
234
  def check_url(url: str, *, insecure: bool = False) -> None:
234
235
  """Refuse what this client never sends: a scheme other than http or https, no host, credentials in the URL, or
235
- plain http to a host that is not loopback (unless `insecure`)."""
236
+ plain http to a host that is not loopback (unless `insecure`). A host `new URL` refuses for its structure (a last
237
+ label that is a number but no IPv4 address, `a127.0.0.1`, `09`; a host UTS46 refuses, a joiner beside ASCII alone) is
238
+ refused as the Node twin refuses it, read by fabric_secrets.url_refused (FAA-187 (14))."""
236
239
  try:
237
240
  # The ends are trimmed as urllib trims what it sends (and `new URL` too); a URL ending in a line break, which
238
241
  # 0.55.0 sent, still passes (0.56.0 product r17).
@@ -242,7 +245,7 @@ def check_url(url: str, *, insecure: bool = False) -> None:
242
245
  parts = urllib.parse.urlsplit(sent)
243
246
  host = parts.hostname
244
247
  parts.port # noqa: B018 - raises on a bad port
245
- if not _authority_ok(parts.netloc):
248
+ if not _authority_ok(parts.netloc) or fabric_secrets.url_refused(sent) or not _idna_sendable(host):
246
249
  raise ValueError("authority")
247
250
  except (ValueError, TypeError):
248
251
  raise HttpError("the URL does not parse", url=redact_url(url), kind="refused")
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """The Fabric hub's door, as a service agent reaches it (fabric-hub/0.1; Fabric ADR-0115 §1, ADR-0149 §4; FAA-12b).
2
3
 
3
4
  A service that answers a Project's requests on the board (`fabric-project-comms/0.1`) needs three things from Fabric's
@@ -96,20 +97,24 @@ def fabric_root(platform: str = sys.platform, env: Optional[Dict[str, str]] = No
96
97
 
97
98
 
98
99
  def _windows_pid_alive(pid: int, kernel: Any = None, last_error: Any = None) -> Optional[bool]:
99
- """pid_alive on Windows, where os.kill(pid, 0) would send CTRL_C_EVENT: OpenProcess with query access only. Refused
100
- with ERROR_ACCESS_DENIED (5), another account's or an elevated process: None, as Node's process.kill(pid, 0)
101
- answers EPERM there (FAA-304); refused otherwise, no process. `kernel` and `last_error` stand in for kernel32 and
100
+ """pid_alive on Windows, where os.kill(pid, 0) would send CTRL_C_EVENT: the check Node's process.kill(pid, 0) makes
101
+ (libuv's uv_kill), asking OpenProcess for the same access (terminate, query, synchronize; never the limited query an
102
+ elevated process of the same user grants, FAA-323). Refused with ERROR_ACCESS_DENIED (5), another account's or an
103
+ elevated process: None, as Node answers EPERM there (FAA-304); refused otherwise, no process; opened, running
104
+ while its exit code is STILL_ACTIVE and a zero wait times out. `kernel` and `last_error` stand in for kernel32 and
102
105
  ctypes.get_last_error in a test."""
103
106
  import ctypes
104
107
  if kernel is None:
105
108
  kernel = ctypes.WinDLL("kernel32", use_last_error=True) # type: ignore[attr-defined]
106
109
  last_error = ctypes.get_last_error # type: ignore[attr-defined]
107
- handle = kernel.OpenProcess(0x1000, False, pid) # PROCESS_QUERY_LIMITED_INFORMATION
110
+ handle = kernel.OpenProcess(0x0001 | 0x0400 | 0x00100000, False, pid) # TERMINATE | QUERY_INFORMATION | SYNCHRONIZE
108
111
  if not handle:
109
112
  return None if last_error() == 5 else False # ERROR_ACCESS_DENIED: not verified, never called running
110
113
  try:
111
114
  code = ctypes.c_ulong()
112
- return bool(kernel.GetExitCodeProcess(handle, ctypes.byref(code))) and code.value == 259 # STILL_ACTIVE
115
+ if not kernel.GetExitCodeProcess(handle, ctypes.byref(code)) or code.value != 259: # STILL_ACTIVE
116
+ return False
117
+ return kernel.WaitForSingleObject(handle, 0) != 0 # WAIT_OBJECT_0: ended, with exit code 259
113
118
  finally:
114
119
  kernel.CloseHandle(handle)
115
120
 
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """Identifier equality (contract DEC-0076): two identifiers are the same when their normal forms are equal.
2
3
 
3
4
  A port of the contract's `src/identifiers.ts`, held to its `fixtures/vectors/identifiers.json`. The normal form of a URI