@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
  """``fabric-package/1`` (contract DEC-0050, DEC-0051, DEC-0071, DEC-0074; FAA-27a): pack an agent or a skill set as one
3
4
  folder with ``fabric.package.json``, sign it, and verify it. The twin of ``fabric-package.mjs``.
4
5
 
@@ -390,12 +391,24 @@ def package_findings(pkg: Dict[str, Any]) -> List[Dict[str, str]]:
390
391
 
391
392
  # --- the folder check and the verifier ------------------------------------------------------------------
392
393
 
394
+ def _json_int(text: str) -> Any:
395
+ """An integer literal as JSON.parse reads it: one longer than a Python with `int_max_str_digits` reads is a float,
396
+ as a JavaScript number is (FAA-194 (36))."""
397
+ return int(text) if len(text) <= 4000 else float(text)
398
+
399
+
393
400
  def _parsed(data: Optional[bytes]) -> Optional[Dict[str, Any]]:
401
+ """A plugin or provider manifest as the contract's checkPackageFolder reads it: decoded with replacement characters
402
+ (`Buffer.toString`, FAA-194 (29)); NaN or Infinity, or nesting past MAX_NESTING (the limit both kits share, FAA-194
403
+ (27)), is no manifest."""
394
404
  if data is None:
395
405
  return None
406
+ text = data.decode("utf-8", "replace")
407
+ if _nesting(text) > MAX_NESTING:
408
+ return None
396
409
  try:
397
- value = json.loads(data.decode("utf-8"), parse_constant=fd._no_constant)
398
- except (ValueError, RecursionError): # invalid UTF-8, NaN or Infinity is no manifest, in both kits
410
+ value = json.loads(text, parse_constant=fd._no_constant, parse_int=_json_int)
411
+ except (ValueError, RecursionError):
399
412
  return None
400
413
  return value if isinstance(value, dict) else None
401
414
 
@@ -419,7 +432,7 @@ def check_package_folder(pkg: Dict[str, Any], files: Dict[str, bytes]) -> List[D
419
432
  problems.append({"problem": "file-unlisted", "path": path})
420
433
  if PLUGIN_MANIFEST in files:
421
434
  plugin = _parsed(files.get(PLUGIN_MANIFEST))
422
- if not plugin or not _same_member(plugin, "name", pkg.get("id", _UNDEFINED)) or not _same_member(plugin, "version", pkg.get("version", _UNDEFINED)):
435
+ if plugin is None or not _same_member(plugin, "name", pkg.get("id", _UNDEFINED)) or not _same_member(plugin, "version", pkg.get("version", _UNDEFINED)):
423
436
  problems.append({"problem": "plugin-manifest", "path": PLUGIN_MANIFEST})
424
437
  provider = pkg.get("provider")
425
438
  if isinstance(provider, str) and provider in files:
@@ -615,7 +628,7 @@ def read_folder(folder: Any) -> Dict[str, bytes]:
615
628
  if _BY_FOLDER:
616
629
  os.close(top) # type: ignore[arg-type]
617
630
  if refused:
618
- raise PackageError("Not packed: remove these or pack a clean copy: " + "; ".join(sorted(refused)))
631
+ raise PackageError("Not packed: pack a clean copy (what Git would commit, references/package.md); delete only litter and credential files in place: " + "; ".join(sorted(refused)))
619
632
  return files
620
633
 
621
634
 
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """fabric-package/1, the schema reading: the package schema bundled from the contract at the pin, read by a JSON Schema
2
3
  2020-12 subset with JavaScript's patterns, lengths in code points and the formats the contract uses, as
3
4
  fabric-package.mjs reads it. fabric_package checks, packs, signs and verifies on it and re-exports these names. Part of
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
2
3
  """A service's pages as Fabric Dashboards' open_link (Fabric Agent Contract DEC-0123, PL-1 to PL-4, FAC-SEM-143;
3
4
  `src/page-link-rules.ts` at the pin): build and read the open_link, tell a raw local address, and the findings on a
4
5
  link a document hands a person and on a URL-mode input request. A port held to the contract's answers by
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """Where a service's things live (fabric-service/0.1): the services directory, a service's data, config, logs and run
2
3
  directories per OS, private directories and atomic writes. Part of the service kit (FAA-65, layer L1)."""
3
4
  from __future__ import annotations
@@ -38,23 +39,35 @@ def services_dir() -> Path:
38
39
  return Path(base) / "passioncode-fabric/services"
39
40
 
40
41
 
42
+ def _durable(service_id: str, dirs: Dict[str, Path]) -> Dict[str, Path]:
43
+ """FABRIC_DATA_HOME (FAA-192): an absolute folder on a durable volume holds the data folder, `<home>/<id>`, on a host
44
+ whose own disk does not persist; logs and cache stay where the OS puts them. A relative value is refused."""
45
+ value = os.environ.get("FABRIC_DATA_HOME")
46
+ if not value:
47
+ return dirs
48
+ if not os.path.isabs(value):
49
+ raise ServiceError("FABRIC_DATA_HOME is an absolute folder on a durable volume.")
50
+ return dict(dirs, data=Path(value) / service_id)
51
+
52
+
41
53
  def service_dirs(service_id: str) -> Dict[str, Path]:
42
- """State lives outside code: data+config, logs and cache in per-user OS directories."""
54
+ """State lives outside code: data+config, logs and cache in per-user OS directories (the data folder under
55
+ FABRIC_DATA_HOME when it is set)."""
43
56
  _require_id(service_id)
44
57
  home = Path.home()
45
58
  if sys.platform == "darwin":
46
- return {
59
+ return _durable(service_id, {
47
60
  "data": home / "Library/Application Support" / service_id,
48
61
  "logs": home / "Library/Logs" / service_id,
49
62
  "cache": home / "Library/Caches" / service_id,
50
- }
63
+ })
51
64
  if sys.platform == "win32":
52
65
  data = _local_app_data() / service_id
53
- return {"data": data, "logs": data / "Logs", "cache": data / "Cache"}
66
+ return _durable(service_id, {"data": data, "logs": data / "Logs", "cache": data / "Cache"})
54
67
  data = Path(os.environ.get("XDG_DATA_HOME") or home / ".local/share") / service_id
55
68
  state = Path(os.environ.get("XDG_STATE_HOME") or home / ".local/state") / service_id
56
69
  cache = Path(os.environ.get("XDG_CACHE_HOME") or home / ".cache") / service_id
57
- return {"data": data, "logs": state / "logs", "cache": cache}
70
+ return _durable(service_id, {"data": data, "logs": state / "logs", "cache": cache})
58
71
 
59
72
 
60
73
  def instance_dirs(service_id: str, instance: str = "default") -> Dict[str, Path]:
@@ -69,6 +82,9 @@ def instance_dirs(service_id: str, instance: str = "default") -> Dict[str, Path]
69
82
  folder, personal = "%s.%s" % (service_id, instance), service_dirs(service_id)
70
83
  if sys.platform == "win32":
71
84
  data = personal["data"].parent / folder
85
+ if os.environ.get("FABRIC_DATA_HOME"): # logs and cache stay on the local disk, beside the default's
86
+ local = personal["logs"].parent.parent / folder
87
+ return {"data": data, "logs": local / "Logs", "cache": local / "Cache"}
72
88
  return {"data": data, "logs": data / "Logs", "cache": data / "Cache"}
73
89
  if sys.platform == "darwin":
74
90
  return {key: path.parent / folder for key, path in personal.items()}
@@ -1,45 +1,38 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """peer_owner: which OS account owns each end of a loopback TCP connection.
2
3
 
3
- peer_owner 1.2.0+kit.6: Observatory's peer_owner 1.2.0 (passioncode-ai/project-observatory-dashboard,
4
- `observatory/engine/peer_owner.py`, first vendored at f86f0622, read against 2180235c; the same licence), the kit's
5
- `fabric_peer_owner` (layer L0; DEC-0154 HA-2, FAA-226, FAA-258). Observatory's 1.3.x (this account's own sockets
6
- through libproc, DEC-0155 RT-2) is not in it (FAA-292). The kit's differences: `Owner` and `Row` are NamedTuples
7
- (a dataclass with postponed annotations needs its module in sys.modules, and a service may load a kit file by
8
- path); `Optional[X]` for `X | None` (the kit's floor is Python 3.9); `classify` / `classify_sid` ask for this
9
- process's own uid or SID first (a client running as root or SYSTEM owns root's listener); on Linux `server_end`
10
- reads an accepted connection's own row and, for one not accept()ed yet (uid 0, inode 0), the listener on its local
11
- address (CT-254; reasons `not-accepted`, `several-rows`, `closed`), `peer` reads a client row with inode 0 as
12
- unknown, several rows of one owner are that owner, a row printed twice counts once, and a `/proc` question that
13
- finds no row or several reads once more; ctypes is imported where used. On macOS (kit.2 to kit.4, FAA-267,
14
- FAA-273): the sysctl calibration also requires a child process's listener, an empty or header-only netstat is
15
- `TABLE_RESTRICTED`, a caller may vouch for a listener with a pid it started (`pid_listens`,
16
- `CheckedHTTPConnection(vouch_pid=...)`), the child's port is read under one deadline with `selectors`, a number is
17
- ASCII digits only. DEC-0155 (kit.6, FAA-312): a netstat that fails, a table with no row of another uid (sysctl) or process
4
+ peer_owner 1.3.1+kit.8: Observatory's peer_owner (passioncode-ai/project-observatory-dashboard,
5
+ `observatory/engine/peer_owner.py`, vendored at f86f0622, read against 2180235c for 1.2.0 and a69c6fee for 1.3.1),
6
+ kit layer L0 (DEC-0154 HA-2, FAA-226, FAA-258). 1.3.1's proof from this account's own sockets (DEC-0155 RT-2) is the
7
+ kit's `fabric_socket_proof`, asked when it sits beside this file (FAA-292, FAA-327); else this file
8
+ stands alone. Kit differences: NamedTuples (a service may load a kit file by path); the Python 3.9 floor; `classify`
9
+ / `classify_sid` ask for this process's uid or SID first (a client running as root owns root's listener); on Linux
10
+ `server_end` reads an accepted connection's own row and, for one not accept()ed yet (uid 0, inode 0), the listener
11
+ on its local address (CT-254), several rows of one owner are that owner, a row printed twice counts once, and a
12
+ `/proc` question that finds no row or several reads once more. On macOS (FAA-267, FAA-273): the sysctl calibration
13
+ also requires a child's listener, read under one deadline with `selectors`; a number is ASCII digits only; a caller
14
+ may vouch for a listener with a pid it started (`pid_listens`, `CheckedHTTPConnection(vouch_pid=...)`, RT-3, after
15
+ RT-2). DEC-0155 (FAA-312): a netstat that fails or is empty, a table with no row of another uid (sysctl) or process
18
16
  (netstat) and a missing connection row read as `table-restricted` (RT-1); a restricted table's listener is this
19
- account's only on exactly 127.0.0.1 (RT-4); the vouch takes exactly 127.0.0.1 (RT-3), never from a set-uid
20
- reader. `main` is the CLI, `listener <port>` (FAA-315). /proc carries a socket's creator, so a listener root created
21
- and handed to another account (systemd socket activation, User=) reads as root's. Standard library only, no other
22
- kit module. Tests: `test/test_fabric_peer_owner.py`; the Node twin is `fabric-peer-owner.mjs` (no ctypes).
17
+ account's only on exactly 127.0.0.1 (RT-4); no proof is taken from a set-uid reader. `main` is the CLI, `listener
18
+ <port>` (FAA-315). /proc carries a socket's creator, so a listener root created and handed to another account
19
+ (socket activation) reads as root's. Tests: `test/test_fabric_peer_owner.py`; Node twin: `fabric-peer-owner.mjs`.
23
20
 
24
- WHY. Loopback is one per machine, not one per OS account. On a host with several accounts, every account's processes
25
- reach every service any account listens with on 127.0.0.1, and a loopback socket proves nothing about who is on the
26
- other end. This module answers that question from the host's socket table, read without privileges:
21
+ WHY. Loopback is one per machine, not per OS account: every account's processes reach every 127.0.0.1 service, and a
22
+ loopback socket proves nothing about the other end. This module reads the host's socket table, without privileges:
27
23
 
28
24
  listener(port) -> Owner who listens on 127.0.0.1:<port> (also 0.0.0.0, ::1, ::)
29
25
  server_end(sock) -> Owner for a client's connected socket: who accepted it
30
26
  peer(sock) -> Owner for a server's accepted socket: who connected
31
27
  peer_at(server, client) the same as `peer`, from two (host, port) addresses
32
- own_connection(port, ...) a CheckedHTTPConnection, connected: every connect, the first and
33
- any reconnect http.client makes after `Connection: close`, checks
34
- that the server end is this account's, or raises ListenerRefused
35
- before a byte of the request is sent
28
+ own_connection(port, ...) a CheckedHTTPConnection, connected: every connect (the first, and any reconnect
29
+ http.client makes) checks the server end is this account's before a byte is sent
36
30
 
37
31
  `Owner.kind` is one of:
38
32
 
39
33
  this-account the socket belongs to the account running this process
40
34
  another-account another OS account's
41
- system uid 0, pid 1 (launchd holds the sockets of socket-activated jobs) or, on
42
- Windows, the system's own accounts and processes
35
+ system uid 0, pid 1 (launchd's socket-activated jobs) or, on Windows, the system's accounts and processes
43
36
  unknown the owner could not be told; `reason` says why
44
37
  none `listener` only: nothing listens on that port
45
38
 
@@ -52,40 +45,32 @@ answer; two live rows with the same addresses are `unknown` (`several-rows`), ne
52
45
 
53
46
  - macOS, primary (`sysctl`): `sysctl net.inet.tcp.pcblist_n` through ctypes, the record list `netstat` itself reads.
54
47
  Each connection is an `xinpcb_n` (addresses and ports) followed by an `xsocket_n` that carries `so_uid` and
55
- `so_last_pid` (XNU `bsd/netinet/in_pcb.h` and `bsd/sys/socketvar.h`, both under `#pragma pack(4)`); the two are
56
- paired by the pcb handle (`xi_inpp` equals `so_pcb`). Measured on macOS 26: about 4 ms for 770 sockets, every pid
57
- equal to the one `netstat` printed. The `xtcpcb_n` record that follows gives the TCP state (`t_state`), so listeners
58
- and TIME_WAIT rows are told apart. The layout is not an API, so the first read in a process calibrates it: it opens
59
- a connected pair of its own (listen, connect, accept) and the listener and both ends of the pair must read back with
60
- their exact addresses, this process's uid and its pid. A layout that fails calibration is not used again in that
61
- process.
62
- - macOS 27.2 shows an ad-hoc-signed interpreter (a Homebrew Python) only its own sockets: the calibration also
63
- requires a child's listener, and a restricted table (DEC-0155 RT-1) is `unknown` with a reason beginning
64
- `table-restricted`, never another account's. A caller that started the server (an installer, its launchd job) may
65
- vouch with that pid: `lsof -p PID` asks the process, which the restriction leaves readable (RT-3).
48
+ `so_last_pid` (XNU `bsd/netinet/in_pcb.h` and `bsd/sys/socketvar.h`, both under `#pragma pack(4)`), paired by the pcb
49
+ handle (`xi_inpp` equals `so_pcb`); the `xtcpcb_n` that follows gives the TCP state. Measured on macOS 26: about 4 ms
50
+ for 770 sockets. The layout is not an API, so the first read in a process calibrates it on a connected pair of its
51
+ own (the listener and both ends must read back with their exact addresses, this uid and this pid); a layout that
52
+ fails is not used again in that process.
53
+ - macOS 27.2 shows an ad-hoc-signed interpreter (a Homebrew Python) only its own sockets: the calibration also requires
54
+ a child's listener, and a restricted table (DEC-0155 RT-1) is `unknown` with a reason beginning `table-restricted`,
55
+ never another account's, unless `fabric_socket_proof` finds the socket in this account's own processes (RT-2) or a
56
+ caller that started the server vouches with its pid, which `lsof -p PID` asks the process itself about (RT-3).
66
57
  - macOS, fallback (`netstat`): `/usr/sbin/netstat -anv -p tcp`, its `process:pid` column, then `ps -o uid= -p PID` for
67
- the owner of the one row a question needs. Measured: about 20 ms over 846 sockets, and it names the pids of root's
68
- listeners, which `lsof` run by an ordinary account cannot see. Another local service implemented the same check this
69
- way; it is kept as the documented fallback for a macOS whose record layout the calibration refuses.
58
+ the one row a question needs; about 20 ms over 846 sockets, and it names the pids of root's listeners, which `lsof`
59
+ run by an ordinary account cannot see. Kept for a macOS whose record layout the calibration refuses.
70
60
  - Linux (`proc`): `/proc/net/tcp` and `/proc/net/tcp6`, whose `uid` column every account may read (also under
71
- `hidepid=2`, which hides `/proc/<pid>` and not this file). No pid: that is only readable for this account's own
72
- processes, and the decision needs the uid alone.
73
- - Windows (`iphlpapi`): `GetExtendedTcpTable(TCP_TABLE_OWNER_PID_ALL)` gives each row's owning pid;
74
- `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)`, `OpenProcessToken(TOKEN_QUERY)` and
75
- `GetTokenInformation(TokenUser)` give its account SID, compared with this process's. A process that cannot be opened
76
- (another account's, usually) is `unknown`, never `another-account`: the caller refuses either way, and the reason
77
- says which it was. `Owner.uid` is None there.
61
+ `hidepid=2`). No pid: that is only readable for this account's own processes, and the decision needs the uid alone.
62
+ - Windows (`iphlpapi`): `GetExtendedTcpTable(TCP_TABLE_OWNER_PID_ALL)` gives each row's owning pid; `OpenProcess`,
63
+ `OpenProcessToken` and `GetTokenInformation(TokenUser)` its account SID, compared with this process's. A process that
64
+ cannot be opened (another account's, usually) is `unknown`, never `another-account`. `Owner.uid` is None there.
78
65
  - Anything else: `unknown`, reason `unsupported-platform`.
79
66
 
80
- `LOCAL_PEERCRED`, `getpeereid` and Linux `SO_PEERCRED` answer this question for Unix-domain sockets only, not for TCP,
81
- which is why a socket table is read here at all.
82
-
83
- WHAT IT NEVER DOES. It never reads a byte from the socket it is asked about, never resolves a name, never sends
84
- anything, and never needs a privilege. It reports the process names `netstat` prints only as far as the pid behind them.
67
+ `LOCAL_PEERCRED`, `getpeereid` and `SO_PEERCRED` cover Unix-domain sockets only, hence a socket table. It never reads
68
+ from the asked socket, resolves a name, sends anything or needs a privilege; of `netstat`'s names, only the pid.
85
69
  """
86
70
  from __future__ import annotations
87
71
 
88
72
  import http.client
73
+ import importlib.util
89
74
  import ipaddress
90
75
  import json
91
76
  import os
@@ -97,8 +82,7 @@ import threading
97
82
  import time
98
83
  from typing import NamedTuple, Optional, Tuple
99
84
 
100
- # This file's own version, for projects that copy it; not the engine's (that is configuration.VERSION).
101
- PEER_OWNER_VERSION = "1.2.0+kit.6" # Observatory's 1.2.0 with the kit's differences above
85
+ PEER_OWNER_VERSION = "1.3.1+kit.8" # this file's own, for projects that copy it: 1.3.1 with the kit's differences above
102
86
 
103
87
  THIS_ACCOUNT = "this-account"
104
88
  ANOTHER_ACCOUNT = "another-account"
@@ -107,8 +91,7 @@ UNKNOWN = "unknown"
107
91
  NONE = "none"
108
92
  KINDS = (THIS_ACCOUNT, ANOTHER_ACCOUNT, SYSTEM, UNKNOWN, NONE)
109
93
 
110
- #: The most table bytes one read accepts: 1 MB held about 1,200 sockets at `pcblist_n`'s 860 bytes each, which a busy
111
- #: developer machine passes; 4 MiB holds about 6,800 at 616 bytes measured on macOS 26 (FAA-260).
94
+ #: The most table bytes one read accepts: about 6,800 sockets at 616 bytes each, measured on macOS 26 (FAA-260).
112
95
  MAX_TABLE_BYTES = 4 * 1024 * 1024
113
96
  #: Every subprocess this module starts is stopped after this many seconds.
114
97
  TIMEOUT_SECONDS = 2.0
@@ -119,8 +102,7 @@ NETSTAT = "/usr/sbin/netstat"
119
102
  PS = "/bin/ps"
120
103
  LSOF = "/usr/sbin/lsof"
121
104
 
122
- #: The reason a socket table hides other processes' sockets from this one (FAA-267: macOS 27.2 shows an ad-hoc-signed
123
- #: interpreter, such as a Homebrew Python, only its own sockets through sysctl and nothing through netstat).
105
+ #: Why a table hides other processes' sockets (FAA-267: macOS 27.2 shows an ad-hoc-signed Python only its own).
124
106
  TABLE_RESTRICTED = "table-restricted"
125
107
  #: A macOS table that lists sockets but not the reader's own connection (DEC-0155 RT-1, FAA-312).
126
108
  NO_ROW_RESTRICTED = TABLE_RESTRICTED + ": no row for this connection"
@@ -131,8 +113,7 @@ SERVER_UNVERIFIED = "server-unverified"
131
113
  SERVER_NOT_THIS_WORKSPACE = "server-not-this-workspace"
132
114
 
133
115
 
134
- # The fields carry real types, not postponed strings: a NamedTuple's generated __new__ resolves its annotations in a
135
- # namespace without builtins, where `int | None` as a string is unresolvable (and a TypeError on Python 3.9).
116
+ # Real types, not postponed strings: a NamedTuple's __new__ cannot resolve `int | None` as a string (3.9: TypeError).
136
117
  _OwnerFields = NamedTuple("_OwnerFields", [("kind", str), ("uid", Optional[int]), ("pid", Optional[int]),
137
118
  ("method", str), ("reason", Optional[str])])
138
119
  _OwnerFields.__new__.__defaults__ = (None, None, "none", None)
@@ -201,11 +182,6 @@ def _posix_uid() -> Optional[int]:
201
182
  return os.getuid() if hasattr(os, "getuid") else None
202
183
 
203
184
 
204
- def _own_pid() -> int:
205
- """This process's pid: a netstat that shows no other process's row is a restricted table (DEC-0155 RT-1)."""
206
- return os.getpid()
207
-
208
-
209
185
  def _set_uid() -> bool:
210
186
  """Real and effective uid differ: such a reader uses none of DEC-0155's proofs (RT-2 to RT-5), the vouch included."""
211
187
  return hasattr(os, "geteuid") and os.geteuid() != _posix_uid()
@@ -248,8 +224,7 @@ def parse_netstat(text: str) -> list[Row]:
248
224
  leading = at - sum(1 for w in names[:at] if w == "Address")
249
225
  trailing = len(names) - at - 1
250
226
  if not any(line.split()[:1] and line.split()[0].startswith("tcp") for line in lines[header + 1:]):
251
- # The caller's own connection is in any table it can see: a header and no tcp row is the restricted table
252
- # (FAA-273 item 3), not `no-row`.
227
+ # A header and no tcp row is the restricted table, not `no-row`: the caller's own row is in any table (FAA-273)
253
228
  raise TableUnreadable(f"{TABLE_RESTRICTED}: netstat printed only its header to this process")
254
229
  rows: list[Row] = []
255
230
  for line in lines[header + 1:]:
@@ -529,10 +504,9 @@ def _child_line(child: "subprocess.Popen[bytes]", deadline: float) -> Optional[b
529
504
 
530
505
 
531
506
  def _child_listener_seen(rows_of) -> Optional[bool]:
532
- """Whether the table shows a listener of a child process of this one: False when it does not (the table shows
533
- this process alone, FAA-267), None when no child could be started (a frozen or embedded host has no interpreter to
534
- start), or it gave no port line within TIMEOUT_SECONDS (silence, a partial or non-numeric line, an early exit, a
535
- pipe that cannot be read). The calibration trusts only True."""
507
+ """Whether the table shows a listener of a child process of this one: False when not (it shows this process alone,
508
+ FAA-267), None when no child could be started (a frozen or embedded host) or it gave no port line within
509
+ TIMEOUT_SECONDS (silence, a partial line, an early exit, an unreadable pipe). The calibration trusts only True."""
536
510
  if not sys.executable or getattr(sys, "frozen", False):
537
511
  return None
538
512
  try:
@@ -552,8 +526,7 @@ def _child_listener_seen(rows_of) -> Optional[bool]:
552
526
  except (TableUnreadable, OSError, struct.error, ValueError):
553
527
  return False
554
528
  finally:
555
- # One deadline for the whole check: a child that gave no port is killed at once, and one that did gets what is
556
- # left of the deadline to exit on its closed stdin before it is killed.
529
+ # One deadline: a child that gave no port is killed at once; one that did has the rest to exit on a closed stdin
557
530
  try:
558
531
  child.stdin.close()
559
532
  if port is None:
@@ -780,7 +753,7 @@ def restricted(rows: list[Row], how: str, *, own_uid: Optional[int] = None, own_
780
753
  own = _posix_uid() if own_uid is None else own_uid
781
754
  return not any(row.uid is not None and row.uid != own for row in rows)
782
755
  if how == "netstat":
783
- own = _own_pid() if own_pid is None else own_pid
756
+ own = os.getpid() if own_pid is None else own_pid
784
757
  return not any(row.pid is not None and row.pid != own for row in rows)
785
758
  return False
786
759
 
@@ -810,10 +783,8 @@ def _owner_of(local: tuple[str, int], remote: tuple[str, int], server: bool = Fa
810
783
  if len(found) > 1:
811
784
  return Owner(UNKNOWN, None, None, how, "several-rows")
812
785
  if how == "proc" and server and found[0].uid is None:
813
- # Linux: a connection not accept()ed yet (uid 0, inode 0) belongs to the listener on its local address (exact,
814
- # else the wildcard) and dies with it (inet_csk_listen_stop); an accepted one's own row carries the accepting
815
- # process's uid (sock_graft), so a listener swapped after another account accepted cannot pass (Observatory's
816
- # 1.2.0, board thread ced00f6e; CT-254). An orphaned server row reads so too: nothing sent to it reaches anyone.
786
+ # Linux: an unaccepted connection (uid 0, inode 0) or an orphaned server row is the listener's on its local address
787
+ # (exact, else wildcard); an accepted row holds the acceptor's uid (sock_graft): no listener swap passes (CT-254).
817
788
  return _one_owner(_listeners_of(rows, found[0].local), how, "not-accepted")
818
789
  if how == "proc" and found[0].uid is None:
819
790
  # A client's socket with inode 0 has already closed (0.74.0 unit reading 4): never another socket's owner.
@@ -842,10 +813,40 @@ def _one_owner(found: list[Row], how: str, none_reason: Optional[str] = None) ->
842
813
  return Owner(UNKNOWN, None, None, how, "several-rows")
843
814
 
844
815
 
816
+ _PROOF: list = [None] # fabric_socket_proof once looked for: None not yet, False not beside this file (FAA-292)
817
+
818
+
819
+ def _proved(ask: str, *args, fallback: Owner, **kwargs) -> Owner:
820
+ """`fallback`, unless it is a restricted macOS table's `unknown` and `fabric_socket_proof` beside this file finds the
821
+ socket (`accepted`) or the 127.0.0.1 listener (`own_listener`, FAA-272) in this account's processes: else it stands."""
822
+ if fallback.kind != UNKNOWN or not (fallback.reason or "").startswith(TABLE_RESTRICTED) or sys.platform != "darwin":
823
+ return fallback
824
+ try:
825
+ if _PROOF[0] is None: # the file beside this one; sys.path stays the host's (FAA-327); alone, the table stands
826
+ here = os.path.realpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "fabric_socket_proof.py"))
827
+ if not os.path.isfile(here):
828
+ raise ImportError(here)
829
+ try: # the host's imported copy if it is that file (one calibration); the statement keeps it in the closure
830
+ import fabric_socket_proof as module
831
+ except ImportError:
832
+ module = None
833
+ if os.path.realpath(getattr(module, "__file__", None) or "-") != here: # else that file, by its path
834
+ spec = importlib.util.spec_from_file_location("fabric_socket_proof", here)
835
+ spec.loader.exec_module(module := importlib.util.module_from_spec(spec))
836
+ _PROOF[0] = module
837
+ proof = getattr(_PROOF[0], ask)(*args, **kwargs) if _PROOF[0] else None
838
+ except ImportError:
839
+ _PROOF[0], proof = False, None
840
+ except Exception: # noqa: BLE001 - a proof that fails proves nothing; the table's answer stands (fail closed)
841
+ proof = None
842
+ return Owner(THIS_ACCOUNT, _posix_uid(), proof.pid, proof.method, proof.reason) if proof and proof.proved else fallback
843
+
844
+
845
845
  def peer_at(server: tuple, client: tuple) -> Owner:
846
846
  """The owner of the CLIENT socket of the connection between `server` and `client`, as a server sees them (its own
847
847
  address, the peer's): the row whose local address is the client's and whose remote address is the server's."""
848
- return _owner_of(_address(client), _address(server))
848
+ client, server = _address(client), _address(server)
849
+ return _proved("accepted", client, server, wait=False, fallback=_owner_of(client, server))
849
850
 
850
851
 
851
852
  def peer(sock: socket.socket) -> Owner:
@@ -859,14 +860,14 @@ def peer(sock: socket.socket) -> Owner:
859
860
 
860
861
  def server_end(sock: socket.socket) -> Owner:
861
862
  """For a client's connected socket: who accepted it. The row whose local address is the server's and whose remote
862
- address is this socket's own, which is the server process's accepted socket, so a listener swapped after `listen`
863
- cannot pass for the one that answers. On Linux a connection not yet accept()ed has no owner of its own (inode 0):
864
- its listener answers (the contract's CT-254, HA-2's Linux source)."""
863
+ address is this socket's own, the server process's accepted socket, so a listener swapped after `listen` cannot
864
+ pass; on Linux a connection not accept()ed yet answers through its listener (CT-254); RT-2 under a restricted table."""
865
865
  try:
866
866
  own, server = sock.getsockname(), sock.getpeername()
867
867
  except OSError as exc:
868
868
  return Owner(UNKNOWN, None, None, "none", f"socket: {type(exc).__name__}")
869
- return _owner_of(_address(server), _address(own), server=True)
869
+ server, own = _address(server), _address(own)
870
+ return _proved("accepted", server, own, wait=True, fallback=_owner_of(server, own, server=True))
870
871
 
871
872
 
872
873
  #: port -> (time.monotonic() of the read, the Owner)
@@ -899,6 +900,7 @@ def listener(port: int) -> Owner:
899
900
  owner = Owner(UNKNOWN, None, None, method(), str(exc))
900
901
  else:
901
902
  owner = _restricted_listener(rows, port, how) if restricted(rows, how) else _one_owner(pick_listener(rows, port), how)
903
+ owner = _proved("own_listener", port, fallback=owner)
902
904
  _LISTENERS[port] = (now, owner)
903
905
  return owner
904
906
 
@@ -922,17 +924,21 @@ def refusal(owner: Owner, port: int, expect_pid: Optional[int] = None) -> Option
922
924
  return None
923
925
 
924
926
 
925
- def pid_listens(pid: int, port: int) -> bool:
926
- """macOS, when the socket table is restricted (FAA-267, DEC-0155 RT-3): whether process `pid`, this account's,
927
- listens on exactly 127.0.0.1:<port> (never the IPv4 wildcard, which `lsof` prints as it prints an IPv6-only one, nor
928
- ::1; FAA-312). `lsof -p` asks the process itself (libproc), which the restriction does not hide. Only a caller that
929
- started `pid` itself (a launchd job it installed) may vouch with it, and never a set-uid one."""
930
- if sys.platform != "darwin" or not isinstance(pid, int) or isinstance(pid, bool) or pid <= 1 or _set_uid():
927
+ def pid_listens(pid: int, port: int, owner: object = THIS_ACCOUNT) -> bool:
928
+ """macOS, a restricted table (FAA-267, DEC-0155 RT-3): whether process `pid` listens on exactly 127.0.0.1:<port>
929
+ (never the IPv4 wildcard, which `lsof` prints as an IPv6-only one, nor ::1), asked of the process (`lsof -p`). Only
930
+ a caller that started `pid` (a launchd job it installed) vouches with it, never a set-uid one for this account. `owner`:
931
+ the uid `pid` runs as, this account's by default, or None when the caller checks it (a LaunchDaemon's UserName)."""
932
+ if owner is not None and owner != THIS_ACCOUNT and (not isinstance(owner, int) or isinstance(owner, bool) or owner < 0):
933
+ raise ValueError("owner is this-account, a uid or None")
934
+ if sys.platform != "darwin" or not isinstance(pid, int) or isinstance(pid, bool) or pid <= 1 or (
935
+ owner == THIS_ACCOUNT and _set_uid()):
931
936
  return False
932
937
  try:
933
- uid = parse_ps_uid(_run([PS, "-o", "uid=", "-p", str(pid)]))
934
- if uid is None or uid != _posix_uid():
935
- return False
938
+ if owner is not None:
939
+ uid = parse_ps_uid(_run([PS, "-o", "uid=", "-p", str(pid)]))
940
+ if uid is None or uid != (_posix_uid() if owner == THIS_ACCOUNT else owner):
941
+ return False
936
942
  out = _run([LSOF, "-nP", "-a", "-p", str(pid), "-i4TCP", "-sTCP:LISTEN", "-Fn"])
937
943
  except TableUnreadable:
938
944
  return False
@@ -940,9 +946,8 @@ def pid_listens(pid: int, port: int) -> bool:
940
946
 
941
947
 
942
948
  class CheckedHTTPConnection(http.client.HTTPConnection):
943
- """An `http.client.HTTPConnection` whose every `connect()` checks the server end before returning: `http.client`
944
- connects again by itself (`auto_open`) after `Connection: close` or a close, and that connection may reach another
945
- listener. A refused connect closes the socket and raises ListenerRefused before any byte of a request is sent."""
949
+ """An `http.client.HTTPConnection` whose every `connect()` (the reconnect `auto_open` makes too, which may reach
950
+ another listener) checks the server end: refused, it closes the socket and raises ListenerRefused before a byte."""
946
951
 
947
952
  def __init__(self, host: str, port: int, *, expect_pid: Optional[int] = None, timeout: float = 10.0,
948
953
  vouch_pid: Optional[int] = None) -> None:
@@ -957,7 +962,7 @@ class CheckedHTTPConnection(http.client.HTTPConnection):
957
962
  super().connect()
958
963
  owner = server_end(self.sock)
959
964
  if (owner.kind == UNKNOWN and (owner.reason or "").startswith(TABLE_RESTRICTED) and getattr(self, "vouch_pid", None)
960
- and pid_listens(self.vouch_pid, self.port)):
965
+ and _address(self.sock.getpeername())[0] == "127.0.0.1" and pid_listens(self.vouch_pid, self.port)):
961
966
  # The table hides other processes (FAA-267); the process this caller started holds the listener.
962
967
  owner = Owner(THIS_ACCOUNT, _posix_uid(), self.vouch_pid, "lsof-pid")
963
968
  refused = refusal(owner, self.port, self.expect_pid)
@@ -968,16 +973,11 @@ class CheckedHTTPConnection(http.client.HTTPConnection):
968
973
 
969
974
  def own_connection(port: int, *, expect_pid: Optional[int] = None, timeout: float = 10.0,
970
975
  host: str = "127.0.0.1", vouch_pid: Optional[int] = None) -> CheckedHTTPConnection:
971
- """A `CheckedHTTPConnection` to `host:port`, connected, whose server end is this account's (and, with `expect_pid`,
972
- that process's). The check runs on the very connection the caller then sends on, so no listener can change between
973
- the check and the request, and again on every reconnect `http.client` makes. No proxy is consulted. Raises
974
- ListenerRefused after closing the connection:
975
-
976
- server-not-own another account's or the system's process accepted it
977
- server-unverified its owner could not be told (nothing is sent: fail closed)
978
- server-not-this-workspace this account's process, but not the pid expected
979
-
980
- `vouch_pid`, as on `CheckedHTTPConnection` and the Node twin's `checkedSocket`: a pid the caller started (FAA-267)."""
976
+ """A `CheckedHTTPConnection` to `host:port`, connected, whose server end is this account's (with `expect_pid`, that
977
+ process's), checked on the connection the caller sends on and on every reconnect; no proxy is consulted.
978
+ ListenerRefused, closed: `server-not-own` (another account's or the system's process), `server-unverified` (owner
979
+ not told: fail closed), `server-not-this-workspace` (not the pid expected). `vouch_pid`: a pid the caller started
980
+ (FAA-267), as on `CheckedHTTPConnection` and the Node twin's `checkedSocket`."""
981
981
  conn = CheckedHTTPConnection(host, port, expect_pid=expect_pid, timeout=timeout, vouch_pid=vouch_pid)
982
982
  conn.connect()
983
983
  return conn
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """Windows, where the kit needs it (DEC-0032): named pipes that hold one copy, protecting a file to its owner, and
2
3
  reading a file's ACL to refuse a token file others can read. On other systems the same names exist and do nothing
3
4
  or answer "absent". Part of the service kit (FAA-65, layer L1)."""
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
2
3
  """Runner and model profiles in the kit (Fabric Agent Contract DEC-0046, Fabric ADR-0147). A port of the contract's
3
4
  `src/profile-rules.ts` at the pin — `resolve_model_profile` and the FAC-SEM-058…060 checks — held to the contract's own
4
5
  answers by test/fixtures/contract/profile-vectors.json; plus what a service does with them: the doctor's runner and
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  """Protocol revisions (DEC-0067) and an event's machine-readable data (DEC-0070, the feed-data protocols): parse,
2
3
  accept, negotiate and check them. Part of the service kit (FAA-65, layer L3)."""
3
4
  from __future__ import annotations