@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 contract's one credential detector (Fabric Agent Contract DEC-0044, `src/secret-shape.ts` at the pin): a name
3
4
  that can only carry a credential, a value shaped like one, a launch argument that carries one, and a URL that does
4
5
  (LC-21). A port held to the contract's own answers by test/fixtures/contract/profile-vectors.json. Standard library
@@ -31,7 +32,7 @@ CREDENTIAL_PAIRS: Dict[str, tuple] = {
31
32
  }
32
33
  #: ECMAScript's \s, as a character class body.
33
34
  JS_SPACE = "\t\n\x0b\x0c\r \xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
34
- _NOT_LINE_END = "[^\n\r\u2028\u2029]" # ECMAScript's `.` without the s flag
35
+ NOT_LINE_END = "[^\n\r\u2028\u2029]" # ECMAScript's `.` without the s flag
35
36
  JWT_SHAPE = re.compile(r"eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*")
36
37
  _URL_BASE64_RUN = re.compile(r"[A-Za-z0-9_-]*")
37
38
 
@@ -74,14 +75,17 @@ _PROVIDER_KEYS = (r"(?:sk-(?=[A-Za-z0-9_-]{0,255}?[0-9])(?:(?=[A-Za-z0-9_-]{0,25
74
75
  #: The contract's SECRET_VALUE at the pin (DEC-0151): `Bearer ` at a value's start in any case, or a provider key after a
75
76
  #: boundary. No case-insensitive flag; digits are ASCII. A match includes its boundary character: callers test, never
76
77
  #: read the matched text or its index.
77
- SECRET_VALUE = re.compile("(^[Bb][Ee][Aa][Rr][Ee][Rr][" + JS_SPACE + "]" + _NOT_LINE_END + "+|" + _KEY_BOUNDARY + _PROVIDER_KEYS + ")")
78
+ SECRET_VALUE = re.compile("(^[Bb][Ee][Aa][Rr][Ee][Rr][" + JS_SPACE + "]" + NOT_LINE_END + "+|" + _KEY_BOUNDARY + _PROVIDER_KEYS + ")")
78
79
  _LONG_RUN = re.compile(r"[A-Za-z0-9+/=_-]{40,}")
79
- _BEARER_INSIDE = re.compile(r"(?<![A-Za-z0-9_])bearer[%s]+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{12,}=*" % JS_SPACE,
80
+ BEARER_INSIDE = re.compile(r"(?<![A-Za-z0-9_])bearer[%s]+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{12,}=*" % JS_SPACE,
80
81
  re.IGNORECASE | re.ASCII)
81
82
  _PATH_LIKE = re.compile(r"^(?:[/~.]|[A-Za-z]:[\\/]|[a-z][a-z0-9+.-]*://)", re.IGNORECASE | re.ASCII)
82
83
  _WORD_SPLIT = re.compile("[%s._-]+" % JS_SPACE)
83
84
  _ARGUMENT_SPLIT = re.compile("[%s=]" % JS_SPACE)
84
- _SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
85
+ SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
86
+ #: The names the check and older readers wrote before these became public (FAA-190 (2), (12)): kept so a module from
87
+ #: an earlier version beside this one still loads.
88
+ _BEARER_INSIDE, _SCHEME, _NOT_LINE_END = BEARER_INSIDE, SCHEME, NOT_LINE_END
85
89
  #: WHATWG's special schemes that carry an authority with user information (file has none).
86
90
  _SPECIAL = ("ftp", "http", "https", "ws", "wss")
87
91
 
@@ -108,13 +112,17 @@ def credential_name(name: str) -> bool:
108
112
  return any(word in words and any(p in words for p in partners) for word, partners in CREDENTIAL_PAIRS.items())
109
113
 
110
114
 
111
- def _long_secret(value: str) -> bool:
115
+ def long_secret(value: str) -> bool:
116
+ """A run of 40 or more base64 characters mixing upper case, lower case and digits (the Node twin's longSecret)."""
112
117
  return any(re.search(r"[a-z]", run) and re.search(r"[A-Z]", run) and re.search(r"[0-9]", run) for run in _LONG_RUN.findall(value))
113
118
 
114
119
 
120
+ _long_secret = long_secret # its name before FAA-190 (2)
121
+
122
+
115
123
  def secret_value(value: str) -> bool:
116
124
  """A JWT anywhere, a provider key or `Bearer …`, or a long base64 run mixing upper case, lower case and digits."""
117
- return bool(has_jwt(value) or SECRET_VALUE.search(value) or _long_secret(value))
125
+ return bool(has_jwt(value) or SECRET_VALUE.search(value) or long_secret(value))
118
126
 
119
127
 
120
128
  def _path_like(piece: str) -> bool:
@@ -126,13 +134,13 @@ def _path_like(piece: str) -> bool:
126
134
  def secret_argument(value: str) -> bool:
127
135
  """The same for a launch argument or a server's command and URL: a long run is looked for between path separators,
128
136
  so a folder path holding a session id is not taken for a key."""
129
- if has_jwt(value) or SECRET_VALUE.search(value) or _BEARER_INSIDE.search(value):
137
+ if has_jwt(value) or SECRET_VALUE.search(value) or BEARER_INSIDE.search(value):
130
138
  return True
131
139
  for piece in _ARGUMENT_SPLIT.split(value):
132
140
  if _path_like(piece):
133
- if any(_long_secret(part) for part in re.split(r"[\\/]", piece)):
141
+ if any(long_secret(part) for part in re.split(r"[\\/]", piece)):
134
142
  return True
135
- elif _long_secret(piece):
143
+ elif long_secret(piece):
136
144
  return True
137
145
  return False
138
146
 
@@ -421,7 +429,7 @@ def _url_credential(value: str, hosts: bool) -> bool:
421
429
  # What WHATWG URL parsing does first: trim C0 controls and space from the ends (never other Unicode spaces, which it
422
430
  # refuses), then remove every tab, CR and LF.
423
431
  value = re.sub(r"[\t\n\r]", "", value.strip(_C0_OR_SPACE))
424
- if not _SCHEME.match(value):
432
+ if not SCHEME.match(value):
425
433
  return False # not an absolute URL (WHATWG URL parsing refuses it)
426
434
  if hosts and url_refused(value):
427
435
  return False # `new URL` refuses its host or port, so the Node twin reads it as an argument only (FAA-309)
@@ -442,7 +450,25 @@ _REPOSITORY_AUTHORITY = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://")
442
450
  #: boundary, so composed after a separator look-behind it would miss a key after `-`, `.` or `_`.
443
451
  _REPOSITORY_KEYS = r"sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,}"
444
452
  _KEY_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))(?:%s)" % _REPOSITORY_KEYS)
445
- _JWT_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))%s" % JWT_SHAPE.pattern)
453
+
454
+
455
+ def jwt_at_a_word(text: str) -> bool:
456
+ """Whether JWT_SHAPE matches at a word's start: the text's start, or after a `.`, `_` or `-` (FAA-321). Read in one
457
+ pass, as jwt_spans: the look-behind expression backtracked from every such `eyJ` of a long run, quadratic in it.
458
+ Every `eyJ` of one run shares that run's end and a shorter first segment, so only a run's first `eyJ` at a word's
459
+ start is tried; an `eyJ` that is not at one is passed over, since a later one of its run may be."""
460
+ at = text.find("eyJ")
461
+ while at >= 0:
462
+ if at and text[at - 1] not in "._-":
463
+ at = text.find("eyJ", at + 1)
464
+ continue
465
+ one = _URL_BASE64_RUN.match(text, at + 3).end()
466
+ if one - at - 3 >= 2 and text[one:one + 1] == ".":
467
+ two = _URL_BASE64_RUN.match(text, one + 1).end()
468
+ if two - one - 1 >= 2 and text[two:two + 1] == ".":
469
+ return True
470
+ at = text.find("eyJ", one)
471
+ return False
446
472
 
447
473
 
448
474
  def repository_name(piece: str) -> bool:
@@ -450,7 +476,7 @@ def repository_name(piece: str) -> bool:
450
476
  key's or JWT's shape (`sk-`, `ghp_`, `xoxb-`, `AKIA`, `eyJ`) at its start or after a `.`, `_` or `-` (0.62.0
451
477
  readings 9 and 10), so a letters-only key is still a key while `task-pipeline-dashboard`, which holds an `sk-` shape
452
478
  inside a word, is a name."""
453
- return bool(_REPOSITORY_NAME.match(piece)) and not _KEY_AT_A_WORD.search(piece) and not _JWT_AT_A_WORD.search(piece)
479
+ return bool(_REPOSITORY_NAME.match(piece)) and not _KEY_AT_A_WORD.search(piece) and not jwt_at_a_word(piece)
454
480
 
455
481
 
456
482
  def ascii_decoded(text: str) -> str:
@@ -38,7 +38,7 @@ unset the walk is used). A committed symbolic link is checked by the path it nam
38
38
  its first 8 KiB) is not read; it is listed. Globs in ROOT's `.fabric-source-ignore` (one per line, `#`
39
39
  comments) and `--exclude` leave matching paths out. The owner's list is `--instance-strings`, else the file
40
40
  `FABRIC_INSTANCE_STRINGS` names, else `passioncode-fabric/instance-strings.txt` in the user's configuration folder
41
- (`$XDG_CONFIG_HOME` when it is absolute, else `~/.config`; `%APPDATA%` on Windows) when it exists: one string per
41
+ (`$XDG_CONFIG_HOME` when it is absolute, else `~/.config`; `%APPDATA%` when absolute on Windows) when it exists: one string per
42
42
  line, a line starting with `#` a comment, `home: <name>` declaring the owner's home name (never neutral), strings
43
43
  shorter than three characters ignored, matched without regard to case. A list inside ROOT
44
44
  is refused: it would travel with the source. The summary says whether Git chose the files or every file was read.
@@ -95,6 +95,9 @@ def _refuse_sibling(step: str, missing: BaseException) -> NoReturn:
95
95
 
96
96
  _WROTE_BYTECODE, sys.dont_write_bytecode = sys.dont_write_bytecode, True # a copy inside the checked tree adds no .pyc
97
97
  _STEP = "fabric_secrets" # the sibling being loaded, so a refusal names it whatever the exception says
98
+ #: The public names of fabric_secrets the check and its command line read (FAA-190 (2), (12)), checked when it loads.
99
+ SECRETS_NAMES = ("BEARER_INSIDE", "JS_SPACE", "NOT_LINE_END", "SCHEME", "credential_name", "has_jwt", "jwt_spans",
100
+ "long_secret", "raw_values", "secret_value", "url_parts")
98
101
  try:
99
102
  import fabric_secrets as _secrets # noqa: E402 - the contract's credential detector (DEC-0044), beside this file
100
103
  _STEP = "fabric_source_ignore"
@@ -105,12 +108,18 @@ try:
105
108
  _STEP = "fabric_source_homes"
106
109
  from fabric_source_homes import ( # noqa: E402,F401 - how a declared home is read, beside this file
107
110
  HOME_ENTRY, _NAME_JOIN, _NAME_START, _SEP, _dashed_home, _name_parts, _owner_homes, _spellings)
111
+ _STEP = "fabric_source_check_cli"
112
+ from fabric_source_check_cli import ( # noqa: E402,F401 - the command line, beside this file (FAA-190 (104))
113
+ LIST_NAME, _Parser, _absolute, _list_path, _print, default_list, run_check)
114
+ _STEP = "fabric_secrets"
115
+ for _name in SECRETS_NAMES: # a detector from another version fails here, never at the first file read (FAA-190 (89))
116
+ if not hasattr(_secrets, _name):
117
+ raise ImportError(_name)
108
118
  except Exception as _missing: # a copy without its siblings, or mixed from two versions: a refusal, never exit 1
109
119
  _refuse_sibling(_STEP, _missing)
110
120
  finally:
111
121
  sys.dont_write_bytecode = _WROTE_BYTECODE
112
122
 
113
- LIST_NAME = "instance-strings.txt"
114
123
  #: The folders `pack` refuses to carry (fabric_package.JUNK); --every-file leaves them and the files below unread.
115
124
  PACK_REFUSES = {".git", "__pycache__"}
116
125
  WALK_SKIP = {".git", "node_modules", "__pycache__", ".venv", "venv", ".tox", ".mypy_cache", ".pytest_cache"}
@@ -145,12 +154,12 @@ SYNTHETIC = ("example", "not-a-key", "notakey", "fake", "dummy", "placeholder",
145
154
  # This check's own key shapes, the detector's before DEC-0151 (unbounded, ASCII case-insensitive), apart from the shared
146
155
  # SECRET_VALUE for parity with the contract's tree check (KEY_SHAPE_SOURCES, `src/source-rules.ts` at the pin; CT-207,
147
156
  # FAA-258). Not the detector's `^bearer\s.+`, which reads a whole header value and would take prose that starts with the
148
- # word: a bearer token is looked for as `_BEARER_INSIDE` does, the word, then a run of 12 or more with a digit.
157
+ # word: a bearer token is looked for as `BEARER_INSIDE` does, the word, then a run of 12 or more with a digit.
149
158
  KEY_SHAPE_SOURCES = ("sk-[A-Za-z0-9_-]{16,}", "gh[pousr]_[A-Za-z0-9]{20,}", "xox[abposr]-[A-Za-z0-9-]{10,}", "AKIA[0-9A-Z]{16}", "glpat-[A-Za-z0-9_-]{20,}")
150
159
  PROVIDER_KEY = re.compile("(" + "|".join(KEY_SHAPE_SOURCES) + ")", re.IGNORECASE | re.ASCII)
151
160
  #: A URL parameter's value read as the contract's tree check reads it (`URL_SECRET_VALUE` in `src/source-rules.ts`): the
152
161
  #: detector's whole-value `^bearer` branch and this check's own unbounded shapes, never the shared DEC-0151 detector.
153
- URL_SECRET_VALUE = re.compile("(^bearer[" + _secrets.JS_SPACE + "]" + _secrets._NOT_LINE_END + "+|" + "|".join(KEY_SHAPE_SOURCES) + ")",
162
+ URL_SECRET_VALUE = re.compile("(^bearer[" + _secrets.JS_SPACE + "]" + _secrets.NOT_LINE_END + "+|" + "|".join(KEY_SHAPE_SOURCES) + ")",
154
163
  re.IGNORECASE | re.ASCII)
155
164
  #: Files that are credentials by their name: pack's own rule (fabric_package.SECRET_FILE and SECRET_FILE_OK, held
156
165
  #: equal by test_source_check), and the kit's token files (`service.token`, fabric_auth; `board.credential`, fabric_hub).
@@ -217,7 +226,7 @@ def _url_secret(url: str, depth: int = 0) -> bool:
217
226
  So: a password of 8 or more mixing letters and digits, or a query or fragment value that, percent-decoded, is shaped
218
227
  like a credential (a provider key, a JWT, a long random run under a credential's name)."""
219
228
  url = url.rstrip(".,;)]}")
220
- if not _secrets._SCHEME.match(url):
229
+ if not _secrets.SCHEME.match(url):
221
230
  return False
222
231
  password, query, fragment = _secrets.url_parts(url)
223
232
  # what says it is synthetic is the value, never the host or the path (`examples.blob.core.windows.net`)
@@ -233,7 +242,7 @@ def _url_secret(url: str, depth: int = 0) -> bool:
233
242
  return True
234
243
  if not _real(value):
235
244
  continue
236
- if (_secrets.credential_name(name) and _secrets._long_secret(value)) or \
245
+ if (_secrets.credential_name(name) and _secrets.long_secret(value)) or \
237
246
  _secrets.has_jwt(value) or URL_SECRET_VALUE.search(value):
238
247
  return True
239
248
  return False
@@ -293,13 +302,13 @@ def _secret_in(line: str) -> Optional[str]:
293
302
  # with a segment ending in `sk` (`task-management-service`) would read as a key.
294
303
  if any(True for _ in _provider_keys(line)):
295
304
  return "a value shaped like a provider key"
296
- if any(_real(m.group(0)) for m in _overlapping(_secrets._BEARER_INSIDE, line)):
305
+ if any(_real(m.group(0)) for m in _overlapping(_secrets.BEARER_INSIDE, line)):
297
306
  return "a bearer token"
298
307
  if "://" in line and any(_url_secret(m.group(0)) for m in URL.finditer(line)):
299
308
  return "a URL that carries a credential"
300
309
  for m in (_assigned(line) if ":" in line or "=" in line else ()):
301
310
  name, value = m.group(1), m.group(2)
302
- if _credential(name, value, _quoted(line, m)) and _secrets._long_secret(unquote(value)) and _real(unquote(value)):
311
+ if _credential(name, value, _quoted(line, m)) and _secrets.long_secret(unquote(value)) and _real(unquote(value)):
303
312
  return "a long random value assigned to a credential's name"
304
313
  return None
305
314
 
@@ -489,10 +498,10 @@ def _raw_secret_spans(text: str) -> List[Tuple[int, int]]:
489
498
  spans += [(a, b) for a, b in _secrets.jwt_spans(text) if _real(text[a:b])] # one pass (FAA-313)
490
499
  spans += [m.span(1) for m in JWK_PRIVATE.finditer(text) if _real(m.group(1))]
491
500
  spans += list(_provider_keys(text))
492
- spans += [m.span() for m in _overlapping(_secrets._BEARER_INSIDE, text) if _real(m.group(0))]
501
+ spans += [m.span() for m in _overlapping(_secrets.BEARER_INSIDE, text) if _real(m.group(0))]
493
502
  spans += [m.span() for m in URL.finditer(text) if _url_secret(m.group(0))]
494
503
  spans += [m.span(2) for m in _assigned(text)
495
- if _credential(m.group(1), m.group(2), _quoted(text, m)) and _secrets._long_secret(unquote(m.group(2)))
504
+ if _credential(m.group(1), m.group(2), _quoted(text, m)) and _secrets.long_secret(unquote(m.group(2)))
496
505
  and _real(unquote(m.group(2)))]
497
506
  return spans
498
507
 
@@ -814,145 +823,9 @@ def check(root: Path, entries: List[str], globs: List[str], every_file: bool = F
814
823
  "dependencies": [shown(d) for d in dependencies]}
815
824
 
816
825
 
817
- def _absolute(path: str, platform: str) -> bool:
818
- return bool(re.match(r"[A-Za-z]:[\\/]|[\\/]{2}", path)) if platform == "win32" else path.startswith("/")
819
-
820
-
821
- def default_list(platform: Optional[str] = None, env: Optional[Dict[str, str]] = None) -> Path:
822
- """Where the owner's list is kept when none is named: the user's configuration folder, never a project's."""
823
- platform = platform or sys.platform
824
- env = os.environ if env is None else env
825
- # a relative value is ignored, as the XDG spec says (a quoted `~/.config` is relative too): never "no list"
826
- if platform == "win32":
827
- appdata = env.get("APPDATA", "")
828
- base = Path(appdata) if appdata and _absolute(appdata, platform) else Path.home() / "AppData" / "Roaming"
829
- else:
830
- xdg = env.get("XDG_CONFIG_HOME", "")
831
- base = Path(xdg) if xdg.startswith("/") else Path.home() / ".config"
832
- return base / "passioncode-fabric" / LIST_NAME
833
-
834
-
835
- def _list_path(given: Optional[str]) -> Tuple[Optional[Path], bool]:
836
- """The owner's list and whether it must be read (a named or an existing default list; else no list)."""
837
- named = given or os.environ.get("FABRIC_INSTANCE_STRINGS")
838
- if named:
839
- try:
840
- return Path(named).expanduser(), True
841
- except (RuntimeError, KeyError): # `~user` with no such user (KeyError on 3.9): never "no finding"
842
- raise CheckError("the owner's list cannot be read")
843
- try:
844
- default = default_list()
845
- except (RuntimeError, KeyError): # no HOME and no account entry (a container's arbitrary user)
846
- raise CheckError("there is no home folder to find the owner's list in: set an absolute XDG_CONFIG_HOME (APPDATA on "
847
- "Windows) or name the list with --instance-strings")
848
- try:
849
- os.stat(str(default))
850
- except (FileNotFoundError, NotADirectoryError):
851
- # a link to a list, its folder or the configuration folder on a volume not mounted: there, but not reachable
852
- if any(os.path.lexists(str(p)) and not os.path.exists(str(p)) for p in (default, *default.parents)):
853
- raise CheckError("the owner's list in your configuration folder cannot be read")
854
- return None, False
855
- except OSError: # a list that is there but cannot be reached is not "no list": that would fail open
856
- raise CheckError("the owner's list cannot be read")
857
- return default, True
858
-
859
-
860
- class _Parser(argparse.ArgumentParser):
861
- def error(self, message: str) -> NoReturn: # argparse would repeat the arguments it could not read
862
- self.print_usage(sys.stderr)
863
- self.exit(2, "fabric_source_check.py: the arguments cannot be read; see the usage above\n")
864
-
865
-
866
- def _print(text: str) -> None:
867
- """Print a line the console may not encode (a name in another script, a byte Git kept as a surrogate)."""
868
- encoding = getattr(sys.stdout, "encoding", None) or "utf-8"
869
- print(text.encode(encoding, "backslashreplace").decode(encoding, "replace"))
870
-
871
-
872
826
  def run(argv: Optional[List[str]] = None) -> int:
873
- parser = _Parser(prog="fabric_source_check.py", description="Find instance data in an agent's source.")
874
- sub = parser.add_subparsers(dest="command", required=True)
875
- cmd = sub.add_parser("check", help="read the source tree and report instance data without repeating it")
876
- cmd.add_argument("root", nargs="?", default=".")
877
- cmd.add_argument("--instance-strings", metavar="FILE")
878
- cmd.add_argument("--exclude", action="append", default=[], metavar="GLOB")
879
- cmd.add_argument("--json", action="store_true")
880
- cmd.add_argument("--every-file", action="store_true", help="read every file, not only what Git would commit")
881
- try:
882
- args = parser.parse_args(argv)
883
- except SystemExit as exc: # --help, or arguments argparse refused: the exit code, as the command line gives it
884
- return exc.code if isinstance(exc.code, int) else 2
885
- try:
886
- try:
887
- root = Path(args.root).expanduser().resolve()
888
- is_folder = root.is_dir()
889
- except (OSError, RuntimeError, KeyError): # a parent it cannot search, a symlink loop, `~` with no home
890
- raise CheckError("the folder cannot be read")
891
- if not is_folder:
892
- raise CheckError("ROOT is not a folder")
893
- list_path, _ = _list_path(args.instance_strings) # may raise CheckError
894
- entries: List[str] = []
895
- if list_path is not None:
896
- try:
897
- resolved = list_path.resolve()
898
- except (OSError, RuntimeError): # RuntimeError: Python 3.9's symlink loop
899
- raise CheckError("the owner's list cannot be read")
900
- if resolved == root or root in resolved.parents:
901
- raise CheckError("the owner's list is inside the tree it checks, where it would travel with the source; "
902
- "keep it in your configuration folder (passioncode-fabric/%s)" % LIST_NAME)
903
- try:
904
- present = resolved.is_file()
905
- except OSError:
906
- raise CheckError("the owner's list cannot be read")
907
- if not present:
908
- raise CheckError("the owner's list named by --instance-strings or FABRIC_INSTANCE_STRINGS does not exist or is "
909
- "not a regular file"
910
- if args.instance_strings or os.environ.get("FABRIC_INSTANCE_STRINGS")
911
- else "the owner's list in your configuration folder is not a file")
912
- try:
913
- entries = read_list(resolved)
914
- except UnicodeDecodeError:
915
- raise CheckError("the owner's list is not UTF-8")
916
- except OSError:
917
- raise CheckError("the owner's list cannot be read")
918
- result = check(root, entries, ignore_globs(root, args.exclude), args.every_file)
919
- except CheckError as exc:
920
- print("fabric_source_check.py: %s" % exc, file=sys.stderr)
921
- return 2
922
- if result.get("manifest"): # under fabric.source.json: its own report (fabric_source_tree.print_run)
923
- return sys.modules["fabric_source_tree"].print_run(result, entries, list_path is not None, args.json, _print)
924
- findings = result["findings"]
925
- skipped = result["skipped"]
926
- listed = sum(1 for e in entries if e and not e.startswith(HOME_ENTRY))
927
- homes = sum(1 for e in entries if e.startswith(HOME_ENTRY))
928
- # a binary file is not read either: it is complete only when every one is named as source in the ignore file
929
- complete = not (skipped["unreadable"] or skipped["binary"] or skipped["nested"]) # type: ignore[index]
930
- result.update({"instanceStrings": listed, "ownerHomes": homes, "instanceList": list_path is not None, "blocking": bool(findings),
931
- "complete": complete})
932
- if args.json:
933
- _print(json.dumps(result, indent=2))
934
- else:
935
- for f in findings: # type: ignore[union-attr]
936
- _print("%s:%s: %s (%s)" % (f["path"], f["line"], f["kind"], f["rule"]))
937
- for n in result["notRead"]: # type: ignore[union-attr]
938
- _print("%s: not read (%s)" % (n["path"], n["why"]))
939
- for folder in result["dependencies"]: # type: ignore[union-attr]
940
- _print("%s: not read (a dependency folder; --every-file reads it)" % folder)
941
- if skipped["dependencies"]: # type: ignore[index]
942
- _print("%d dependency folder(s) not read (node_modules, virtual environments, caches); --every-file reads "
943
- "them" % skipped["dependencies"]) # type: ignore[index]
944
- _print("%d file(s) read (%s), %d finding(s); not read: %d binary, %d unreadable, %d nested repository; "
945
- "%d left out by name; %d instance string(s) listed%s%s"
946
- % (result["files"], "what Git would commit" if result["mode"] == "git" else
947
- "every file, as pack reads them" if args.every_file else "every file but dependency folders, Git not used",
948
- len(findings), skipped["binary"], skipped["unreadable"], skipped["nested"], # type: ignore[index]
949
- skipped["excluded"], listed, # type: ignore[index]
950
- "" if listed or homes else (" (the owner's list holds no string of three or more characters)"
951
- if list_path else " (no owner's list)"),
952
- "; %d home name(s) declared" % homes if homes else ""))
953
- if findings:
954
- return 1
955
- return 0 if complete else 2
827
+ """The command line (fabric_source_check_cli.run_check), run with this module's check."""
828
+ return run_check(argv, check, read_list, ignore_globs, HOME_ENTRY)
956
829
 
957
830
 
958
831
  if __name__ == "__main__":
@@ -0,0 +1,164 @@
1
+ """The command line of fabric_source_check.py (FAA-190 (104)): where the owner's list is found, the arguments, and the
2
+ printed report. It sits beside fabric_source_check.py, which imports it inside its import guard and keeps the entry
3
+ point (`fabric_source_check.py check ...`, `fabric_source_check.run`); run the check by path, never this file.
4
+ Standard library only."""
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+ import re
12
+ import sys
13
+ from typing import Callable, Dict, List, NoReturn, Optional, Tuple
14
+
15
+ from fabric_source_ignore import CheckError
16
+
17
+ LIST_NAME = "instance-strings.txt"
18
+
19
+
20
+ def _absolute(path: str, platform: str) -> bool:
21
+ return bool(re.match(r"[A-Za-z]:[\\/]|[\\/]{2}", path)) if platform == "win32" else path.startswith("/")
22
+
23
+
24
+ def default_list(platform: Optional[str] = None, env: Optional[Dict[str, str]] = None) -> Path:
25
+ """Where the owner's list is kept when none is named: the user's configuration folder, never a project's."""
26
+ platform = platform or sys.platform
27
+ env = os.environ if env is None else env
28
+ # a relative value is ignored, as the XDG spec says (a quoted `~/.config` is relative too): never "no list"
29
+ if platform == "win32":
30
+ appdata = env.get("APPDATA", "")
31
+ base = Path(appdata) if appdata and _absolute(appdata, platform) else Path.home() / "AppData" / "Roaming"
32
+ else:
33
+ xdg = env.get("XDG_CONFIG_HOME", "")
34
+ base = Path(xdg) if xdg.startswith("/") else Path.home() / ".config"
35
+ return base / "passioncode-fabric" / LIST_NAME
36
+
37
+
38
+ def _list_path(given: Optional[str]) -> Tuple[Optional[Path], bool]:
39
+ """The owner's list and whether it must be read (a named or an existing default list; else no list)."""
40
+ named = given or os.environ.get("FABRIC_INSTANCE_STRINGS")
41
+ if named:
42
+ try:
43
+ return Path(named).expanduser(), True
44
+ except (RuntimeError, KeyError): # `~user` with no such user (KeyError on 3.9): never "no finding"
45
+ raise CheckError("the owner's list cannot be read")
46
+ try:
47
+ default = default_list()
48
+ except (RuntimeError, KeyError): # no HOME and no account entry (a container's arbitrary user)
49
+ raise CheckError("there is no home folder to find the owner's list in: set an absolute XDG_CONFIG_HOME (APPDATA on "
50
+ "Windows) or name the list with --instance-strings")
51
+ try:
52
+ os.stat(str(default))
53
+ except (FileNotFoundError, NotADirectoryError):
54
+ # a link to a list, its folder or the configuration folder on a volume not mounted: there, but not reachable
55
+ if any(os.path.lexists(str(p)) and not os.path.exists(str(p)) for p in (default, *default.parents)):
56
+ raise CheckError("the owner's list in your configuration folder cannot be read")
57
+ return None, False
58
+ except OSError: # a list that is there but cannot be reached is not "no list": that would fail open
59
+ raise CheckError("the owner's list cannot be read")
60
+ return default, True
61
+
62
+
63
+ class _Parser(argparse.ArgumentParser):
64
+ def error(self, message: str) -> NoReturn: # argparse would repeat the arguments it could not read
65
+ self.print_usage(sys.stderr)
66
+ self.exit(2, "fabric_source_check.py: the arguments cannot be read; see the usage above\n")
67
+
68
+
69
+ def _print(text: str) -> None:
70
+ """Print a line the console may not encode (a name in another script, a byte Git kept as a surrogate)."""
71
+ encoding = getattr(sys.stdout, "encoding", None) or "utf-8"
72
+ print(text.encode(encoding, "backslashreplace").decode(encoding, "replace"))
73
+
74
+
75
+ def run_check(argv: Optional[List[str]], check: Callable[..., Dict[str, object]], read_list: Callable[[Path], List[str]],
76
+ ignore_globs: Callable[[Path, List[str]], List[str]], home_entry: str) -> int:
77
+ """The command line, run with the check's own functions (fabric_source_check.run passes them)."""
78
+ parser = _Parser(prog="fabric_source_check.py", description="Find instance data in an agent's source.")
79
+ sub = parser.add_subparsers(dest="command", required=True)
80
+ cmd = sub.add_parser("check", help="read the source tree and report instance data without repeating it")
81
+ cmd.add_argument("root", nargs="?", default=".", help="the folder to check (default: the current folder)")
82
+ cmd.add_argument("--instance-strings", metavar="FILE", help="the owner's list (default: FABRIC_INSTANCE_STRINGS, else "
83
+ "passioncode-fabric/%s in the user's configuration folder)" % LIST_NAME)
84
+ cmd.add_argument("--exclude", action="append", default=[], metavar="GLOB",
85
+ help="leave matching paths out, as a .fabric-source-ignore line does (repeatable)")
86
+ cmd.add_argument("--json", action="store_true", help="one JSON object: mode, files, findings, notRead, skipped, "
87
+ "dependencies, instanceStrings, ownerHomes, instanceList, blocking, complete; exit 0 clean and "
88
+ "complete, 1 a finding, 2 a file not read or no run")
89
+ cmd.add_argument("--every-file", action="store_true", help="read every file, not only what Git would commit")
90
+ try:
91
+ args = parser.parse_args(argv)
92
+ except SystemExit as exc: # --help, or arguments argparse refused: the exit code, as the command line gives it
93
+ return exc.code if isinstance(exc.code, int) else 2
94
+ try:
95
+ try:
96
+ root = Path(args.root).expanduser().resolve()
97
+ is_folder = root.is_dir()
98
+ except (OSError, RuntimeError, KeyError): # a parent it cannot search, a symlink loop, `~` with no home
99
+ raise CheckError("the folder cannot be read")
100
+ if not is_folder:
101
+ raise CheckError("ROOT is not a folder")
102
+ list_path, _ = _list_path(args.instance_strings) # may raise CheckError
103
+ entries: List[str] = []
104
+ if list_path is not None:
105
+ try:
106
+ resolved = list_path.resolve()
107
+ except (OSError, RuntimeError): # RuntimeError: Python 3.9's symlink loop
108
+ raise CheckError("the owner's list cannot be read")
109
+ if resolved == root or root in resolved.parents:
110
+ raise CheckError("the owner's list is inside the tree it checks, where it would travel with the source; "
111
+ "keep it in your configuration folder (passioncode-fabric/%s)" % LIST_NAME)
112
+ try:
113
+ present = resolved.is_file()
114
+ except OSError:
115
+ raise CheckError("the owner's list cannot be read")
116
+ if not present:
117
+ raise CheckError("the owner's list named by --instance-strings or FABRIC_INSTANCE_STRINGS does not exist or is "
118
+ "not a regular file"
119
+ if args.instance_strings or os.environ.get("FABRIC_INSTANCE_STRINGS")
120
+ else "the owner's list in your configuration folder is not a file")
121
+ try:
122
+ entries = read_list(resolved)
123
+ except UnicodeDecodeError:
124
+ raise CheckError("the owner's list is not UTF-8")
125
+ except OSError:
126
+ raise CheckError("the owner's list cannot be read")
127
+ result = check(root, entries, ignore_globs(root, args.exclude), args.every_file)
128
+ except CheckError as exc:
129
+ print("fabric_source_check.py: %s" % exc, file=sys.stderr)
130
+ return 2
131
+ if result.get("manifest"): # under fabric.source.json: its own report (fabric_source_tree.print_run)
132
+ return sys.modules["fabric_source_tree"].print_run(result, entries, list_path is not None, args.json, _print)
133
+ findings = result["findings"]
134
+ skipped = result["skipped"]
135
+ listed = sum(1 for e in entries if e and not e.startswith(home_entry))
136
+ homes = sum(1 for e in entries if e.startswith(home_entry))
137
+ # a binary file is not read either: it is complete only when every one is named as source in the ignore file
138
+ complete = not (skipped["unreadable"] or skipped["binary"] or skipped["nested"]) # type: ignore[index]
139
+ result.update({"instanceStrings": listed, "ownerHomes": homes, "instanceList": list_path is not None, "blocking": bool(findings),
140
+ "complete": complete})
141
+ if args.json:
142
+ _print(json.dumps(result, indent=2))
143
+ else:
144
+ for f in findings: # type: ignore[union-attr]
145
+ _print("%s:%s: %s (%s)" % (f["path"], f["line"], f["kind"], f["rule"]))
146
+ for n in result["notRead"]: # type: ignore[union-attr]
147
+ _print("%s: not read (%s)" % (n["path"], n["why"]))
148
+ for folder in result["dependencies"]: # type: ignore[union-attr]
149
+ _print("%s: not read (a dependency folder; --every-file reads it)" % folder)
150
+ if skipped["dependencies"]: # type: ignore[index]
151
+ _print("%d dependency folder(s) not read (node_modules, virtual environments, caches); --every-file reads "
152
+ "them" % skipped["dependencies"]) # type: ignore[index]
153
+ _print("%d file(s) read (%s), %d finding(s); not read: %d binary, %d unreadable, %d nested repository; "
154
+ "%d left out by name; %d instance string(s) listed%s%s"
155
+ % (result["files"], "what Git would commit" if result["mode"] == "git" else
156
+ "every file, as pack reads them" if args.every_file else "every file but dependency folders, Git not used",
157
+ len(findings), skipped["binary"], skipped["unreadable"], skipped["nested"], # type: ignore[index]
158
+ skipped["excluded"], listed, # type: ignore[index]
159
+ "" if listed or homes else (" (the owner's list holds no string of three or more characters)"
160
+ if list_path else " (no owner's list)"),
161
+ "; %d home name(s) declared" % homes if homes else ""))
162
+ if findings:
163
+ return 1
164
+ return 0 if complete else 2
@@ -6,10 +6,10 @@ description: >-
6
6
  for a provider manifest (adapting-projects-to-fabric), a new agent (creating-fabric-agents) or a session
7
7
  (working-with-fabric).
8
8
  license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
9
- compatibility: Python 3.9+ or Node.js 20+ for the kits (Node.js 22+ for a Linux service's instance lock); the probe needs Python 3.9+. launchd steps are macOS-only; Linux services install a systemd user unit and Windows ones a per-user Scheduled Task with the kits' writers. Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
9
+ compatibility: Python 3.9+ or Node.js 20+ for the kits (Node.js 22+ for a Linux service's instance lock); the probe needs Python 3.9+. launchd steps are macOS-only; Linux services install a systemd user unit and Windows ones a per-user Scheduled Task with the kits' writers. Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional. On macOS 27.2 run kit services and the probe on Apple's /usr/bin/python3.
10
10
  metadata:
11
11
  author: PassionCode.ai
12
- version: "0.82.0"
12
+ version: "0.84.1"
13
13
  contract-version: "0.1.0"
14
14
  extension: "fabric-service/0.1"
15
15
  extension-commit: "93e7db164c7829e2783b4b7705c4113bb5201131"
@@ -52,7 +52,8 @@ to review one. Do not use it for:
52
52
 
53
53
  It is a service when at least one is true: other agents call it while the operator is
54
54
  away; it runs jobs longer than one request; it owns a store other tools read; the
55
- operator watches it on a page. Otherwise build a CLI and stop here.
55
+ operator watches it on a page; it is an uptime A agent that answers while the owner's computer is off (an online
56
+ service, the remote placement below). Otherwise build a CLI and stop here.
56
57
 
57
58
  Answer before writing code, and record the answers in the project README:
58
59
 
@@ -73,7 +74,7 @@ Answer before writing code, and record the answers in the project README:
73
74
  | Another agent on this machine | MCP `2026-07-28`, `streamable-http`, at `/mcp` on the service origin, token in a header | one process serves every session; `mcp-register` checks the listener (HA-2) first |
74
75
  | The operator or a script | a CLI: `<tool> service status\|start\|stop\|restart`, `<tool> dashboard` (opens only in Fabric Dashboards, below), `<tool> doctor --json` | every verb delegates to launchd and the well-known document, never to its own process table |
75
76
  | A remote agent | A2A `1.0` over HTTPS, or MCP behind an authenticated gateway | loopback reachability is not authorization |
76
- | An A2A peer on this computer | `surfaces.a2a` names a public Agent Card, served without the token; its interface URLs are loopback (DEC-0122); the kits read it, serving one is FAA-10 | the card carries no credential, process member or required Fabric extension |
77
+ | An A2A peer on this computer | `surfaces.a2a` names a public Agent Card, served without the token; its interface URLs are loopback (DEC-0122); the kits read it and serve one with the JSON-RPC binding ([the A2A reference](references/a2a.md)) | the card carries no credential, process member or required Fabric extension |
77
78
  | The dashboard page | same-origin REST under `/api`, session cookie plus a custom request header; every button on it also a tool on the MCP surface (Step 5) | the existing dashboards' CSRF defence |
78
79
 
79
80
  Read [the surfaces and auth reference](references/surfaces-and-auth.md) when wiring
@@ -105,6 +106,7 @@ When the service answers requests other Projects post to its Project on Fabric's
105
106
  responder in its own process: [`scripts/fabric_hub.py`](scripts/fabric_hub.py) finds the hub and joins,
106
107
  [`scripts/fabric_board.py`](scripts/fabric_board.py) enrolls, claims, accepts the exact digest, checkpoints before
107
108
  every step and stops on `fenced`, `lease_expired` or `authority_changed` (Node: `fabric-hub.mjs`, `fabric-board.mjs`).
109
+ It reads an operator's mandate on a request only when it opts in with `agent_id=` and `mandate_from=` (Node: `agentId`, `mandateFrom`).
108
110
  The request body is data behind the service's own input gate. A slot moved to another
109
111
  session stays moved until `take_back` (`takeBack`) or a move back once Fabric offers one: give it an operator action with its tool and gate
110
112
  `relayed` (the operator reaches it through an agent that relays their decision in `_meta`, the service's
@@ -172,7 +174,8 @@ spend ceiling before every attempt, receipts and a fallback). The draft-and-chec
172
174
  support agent on the kit: [`recipes/support-agent/`](recipes/support-agent/README.md); the whole agent:
173
175
  creating-fabric-agents' `references/agents-that-answer-people.md`. A service that is only a dashboard:
174
176
  [`recipes/dashboard-only/`](recipes/dashboard-only/README.md); a door that hands a key on sealed:
175
- [`recipes/key-delivery-door/`](recipes/key-delivery-door/README.md).
177
+ [`recipes/key-delivery-door/`](recipes/key-delivery-door/README.md); numbers computed by code from a read-only
178
+ source: [`recipes/data-agent/`](recipes/data-agent/README.md).
176
179
 
177
180
  Startup order is the part that goes wrong; keep it exactly:
178
181
 
@@ -228,7 +231,7 @@ The installer, not the service, owns the plist and the descriptor. Sequence:
228
231
  FileVault restart) install a LaunchDaemon instead: the plist in `/Library/LaunchDaemons` with `user_name`, as root
229
232
  (the lifecycle reference, *The system domain*).
230
233
  3. Uninstall: `launchd_uninstall` (waits for the unload, removes the plist, resets the
231
- override; `purge=True` removes data only when the operator asks), `remove_descriptor`.
234
+ override; `purge=True` removes data only when the operator asks, and a LaunchDaemon refuses it), `remove_descriptor`.
232
235
  4. After an upgrade, `prune_releases(releases_dir, current=…)` keeps the running release
233
236
  and the one before it.
234
237
 
@@ -268,6 +271,9 @@ agents, models, connectors, stores and channels, each with its state, from
268
271
  `connections_extension()` / `connectionsExtension()` in `fabric_connections`. Never a credential in it, never a call
269
272
  count (that is the usage report); details: [the wire reference](references/protocol.md).
270
273
 
274
+ A service counts its own log into hourly and daily **operations digests** (DEC-0153) with `fabric_ops_digest`
275
+ (`fabric-ops-digest.mjs`): [the ops digest reference](references/ops-digest.md).
276
+
271
277
  The events feed is a **view over the log you already keep** (a jobs table, a JSONL
272
278
  journal), not a second store. Each event is one sentence a person reads; set
273
279
  `notify: true` only when the operator must decide, or something failed or degrades their
@@ -355,7 +361,8 @@ Whoever owns the repository, an agent with a page gets the dashboard rule block
355
361
  (below). org-index `scripts/check_format.py` reports 0 findings for it before it is called done. **An agent
356
362
  someone builds for themselves is not a PassionCode.ai repository:** its licence is its owner's choice, the organization
357
363
  publishes and lists nothing about it, of these files one built new needs only a `LICENSE` naming its copyright holder (copied kit files keep
358
- theirs), and the verified MCP quick start teaches anyone to drive it.
364
+ theirs, each service kit module in its own SPDX line), and the verified MCP quick start teaches anyone to drive it. The kit's files stay AGPL-3.0-only or the
365
+ PassionCode commercial licence there, in an agent built for a client too.
359
366
 
360
367
  ## Online services: the remote placement
361
368
 
@@ -421,7 +428,7 @@ this estate needs.
421
428
  explicitly in the plist; never copy the whole shell environment into it.
422
429
  - A plist pointing at a worktree or checkout breaks when it moves: point it at a release.
423
430
  - `http.server.HTTPServer` resolves its FQDN between `bind()` and `listen()`: with a slow
424
- resolver (a macOS CI runner, a Mac offline) the port is bound but silent. Use `fs.LoopbackHTTPServer`.
431
+ resolver (a macOS CI runner, a Mac offline) the port is bound but silent: connects time out. Use `fs.LoopbackHTTPServer`.
425
432
  - `logging.basicConfig` called twice: the second is a no-op and the log stays empty. Configure logging once, in
426
433
  the entry point.
427
434
  - Hand-made `.plist.bak` files in `~/Library/LaunchAgents` are never cleaned up; keep