@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.
- package/CHANGELOG.md +169 -3
- package/README.md +7 -3
- package/SECURITY.md +4 -3
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +7 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +18 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +16 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_identifiers.py +1 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +31 -15
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +38 -12
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +21 -148
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check_cli.py +164 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +15 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +4 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +14 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/README.md +76 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/config.example.json +28 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/data_agent.py +366 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +32 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +4 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support-agent.mjs +115 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +63 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/a2a.md +98 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +28 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +13 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +11 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/ops-digest.md +75 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +7 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +10 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +70 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_a2a.py +158 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a-server.mjs +275 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a.mjs +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +55 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +47 -31
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +25 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +27 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +19 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate-rules.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +7 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +22 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +4 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-identifiers.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +21 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +25 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ops-digest.mjs +456 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package-schema.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +18 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-page-links.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +19 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +56 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-socket-proof.mjs +181 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +20 -15
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet-identity.mjs +469 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a.py +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a_server.py +405 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +17 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +98 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +37 -48
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +25 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +28 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +19 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +24 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate_rules.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +32 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +19 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +10 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_identifiers.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +56 -96
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +99 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +16 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_mandate.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +44 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ops_digest.py +648 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +17 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package_schema.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_page_links.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +21 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +108 -108
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +38 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_socket_proof.py +391 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +19 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet_identity.py +493 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +632 -104
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample-remote-service.mjs +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +17 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/tailnet-groups.schema.json +69 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +6 -5
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +16 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +14 -9
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +4 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +18 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +4 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/admit.py +182 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +14 -9
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +7 -4
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +10 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +2 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +16 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +79 -8
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +147 -45
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py
CHANGED
|
@@ -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
|
-
|
|
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 + "]" +
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
141
|
+
if any(long_secret(part) for part in re.split(r"[\\/]", piece)):
|
|
134
142
|
return True
|
|
135
|
-
elif
|
|
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
|
|
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
|
-
|
|
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
|
|
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 `
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
874
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|