@passioncode-ai/fabric-agent-adapter 0.39.1 → 0.41.0
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 +203 -0
- package/README.md +18 -6
- package/SECURITY.md +27 -6
- package/bin/fabric-agent-adapter.js +71 -6
- package/package.json +2 -2
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +42 -66
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +4 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +55 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +9 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/verification.md +15 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +181 -26
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +66 -5
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +81 -23
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +42 -33
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +21 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +104 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +15 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/knowledge.example.json +13 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +465 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +21 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +93 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +5 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +129 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +5 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +48 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +66 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +11 -184
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +204 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +53 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +288 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +327 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +47 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +79 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +145 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +848 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +340 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +42 -1237
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +7 -18
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +41 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +15 -46
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +387 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +11 -68
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +52 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +231 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +50 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +67 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +235 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +136 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +335 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +146 -32
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +69 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +164 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -32
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +122 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +177 -2541
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +404 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +46 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +458 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +408 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +399 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +91 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +64 -1398
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +116 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +162 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +882 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +457 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +36 -39
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +336 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +113 -222
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +586 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +44 -405
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +140 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +402 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +119 -77
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +310 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_kit.py +243 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +308 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +202 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +431 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +285 -109
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +102 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +338 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +32 -39
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +173 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +298 -3450
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +8 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +437 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +120 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +697 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +1621 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +25 -7
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +29 -60
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +4 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +3 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +55 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +66 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +3 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/native-host-hooks.md +2 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +26 -13
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +122 -1392
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +278 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +539 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py +324 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +173 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +491 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +11 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/context.mjs +25 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/openclaw/index.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/opencode/fabric.mjs +2 -1
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py
CHANGED
|
@@ -16,12 +16,13 @@ library only; the answer is JSON on stdout, exit 0 when it holds, 1 when it does
|
|
|
16
16
|
|
|
17
17
|
import argparse
|
|
18
18
|
import base64
|
|
19
|
+
import errno
|
|
19
20
|
import hashlib
|
|
20
21
|
import json
|
|
21
22
|
import os
|
|
22
23
|
import re
|
|
24
|
+
import stat as _stat
|
|
23
25
|
import sys
|
|
24
|
-
from datetime import datetime
|
|
25
26
|
from pathlib import Path
|
|
26
27
|
from typing import Any, Dict, List, Optional, Tuple
|
|
27
28
|
|
|
@@ -35,12 +36,20 @@ SIGNATURE_STATEMENT = "fabric-package-signature/1"
|
|
|
35
36
|
# DEC-0051: ES256 joins when Fabric records its decision D2; until then a verifier refuses it as unknown-algorithm.
|
|
36
37
|
PACKAGE_ALGORITHMS = ("EdDSA",)
|
|
37
38
|
SCHEMA_PATH = Path(__file__).resolve().parent / "fabric-package.schema.json"
|
|
38
|
-
# What pack refuses to list: version-control and cache litter, and files that are credentials by their name.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
# What pack refuses to list: version-control and cache litter, and files that are credentials by their name. Matched
|
|
40
|
+
# whole (fullmatch) and with JavaScript's `.`, so a name ending in a newline is judged as fabric-package.mjs judges it.
|
|
41
|
+
_JS_DOT = "[^\n\r\u2028\u2029]"
|
|
42
|
+
JUNK = re.compile(r"\.git|__pycache__|\.DS_Store|%s*\.pyc" % _JS_DOT)
|
|
43
|
+
SECRET_FILE = re.compile(r"\.env(\.%s+)?|%s*\.(pem|key|p12|pfx|jks|keystore)|id_(rsa|dsa|ecdsa|ed25519)|\.(npmrc|netrc|pypirc)" % (_JS_DOT, _JS_DOT))
|
|
44
|
+
SECRET_FILE_OK = re.compile(r"\.env\.(example|sample|template)")
|
|
45
|
+
_B64URL = re.compile(r"[A-Za-z0-9_-]+")
|
|
43
46
|
_PKCS8_ED25519 = bytes.fromhex("302e020100300506032b657004220420")
|
|
47
|
+
#: How deep a document read from disk may nest. Python's recursion limit and Node's stack give out at different depths,
|
|
48
|
+
#: so both readers refuse past this one, well below either (FAA-78, finding 43); no package document comes near it.
|
|
49
|
+
MAX_NESTING = 256
|
|
50
|
+
#: JavaScript's whitespace, what String.prototype.trim and the regular expression class \s take (ECMA-262 WhiteSpace
|
|
51
|
+
#: and LineTerminator), so a key file and a URI are read as fabric-package.mjs reads them.
|
|
52
|
+
JS_WHITESPACE = "\t\n\x0b\x0c\r \xa0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
|
|
44
53
|
|
|
45
54
|
|
|
46
55
|
class PackageError(Exception):
|
|
@@ -63,46 +72,30 @@ def sha256_of(data: Any) -> str:
|
|
|
63
72
|
|
|
64
73
|
def package_digest(pkg: Dict[str, Any]) -> str:
|
|
65
74
|
"""SHA-256 over the UTF-8 RFC 8785 form of the package without ``digest`` and ``signatures`` (DEC-0050).
|
|
66
|
-
Raises TypeError for a value with no RFC 8785 form."""
|
|
75
|
+
Raises TypeError for a value with no RFC 8785 form, a value nested past the stack included."""
|
|
67
76
|
covered = {k: v for k, v in pkg.items() if k not in ("digest", "signatures")}
|
|
68
|
-
|
|
77
|
+
try:
|
|
78
|
+
return sha256_of(_jsv.jcs(covered))
|
|
79
|
+
except RecursionError: # nested past Python's stack: no form this kit can write, as Node's RangeError is none
|
|
80
|
+
raise TypeError("the package nests too deeply to have an RFC 8785 form") from None
|
|
69
81
|
|
|
70
82
|
|
|
71
83
|
def package_signing_input(digest: str, role: str) -> bytes:
|
|
72
84
|
"""The bytes a package signature covers (DEC-0051); the role is inside, so no signature stands for another role."""
|
|
73
|
-
return
|
|
74
|
-
|
|
85
|
+
return _jsv.jcs({"type": SIGNATURE_STATEMENT, "role": role, "digest": digest}).encode("utf-8")
|
|
75
86
|
|
|
76
|
-
# --- Ed25519 signing (RFC 8032 §5.1.6) ------------------------------------------------------------------
|
|
77
|
-
|
|
78
|
-
def _secret_scalar(seed: bytes) -> Tuple[int, bytes]:
|
|
79
|
-
h = hashlib.sha512(seed).digest()
|
|
80
|
-
a = int.from_bytes(h[:32], "little")
|
|
81
|
-
a &= (1 << 254) - 8
|
|
82
|
-
a |= 1 << 254
|
|
83
|
-
return a, h[32:]
|
|
84
87
|
|
|
88
|
+
# --- Ed25519 signing: the core's (fabric_crypto, RFC 8032 §5.1.6), which never branches on the secret (finding 45) ---
|
|
85
89
|
|
|
86
90
|
def ed25519_public_key(seed: bytes) -> bytes:
|
|
87
91
|
"""The 32-byte public key of a 32-byte Ed25519 seed."""
|
|
88
92
|
if len(seed) != 32:
|
|
89
93
|
raise PackageError("An Ed25519 private key is 32 bytes.")
|
|
90
|
-
|
|
91
|
-
return fd._encode_point(fd._mul(a, fd._G))
|
|
92
|
-
|
|
94
|
+
return _crypto.ed25519_seed_public_key(seed)
|
|
93
95
|
|
|
94
|
-
def ed25519_sign(seed: bytes, message: bytes) -> bytes:
|
|
95
|
-
"""A deterministic Ed25519 signature; the same seed and message give the bytes Node's crypto.sign gives."""
|
|
96
|
-
a, prefix = _secret_scalar(seed)
|
|
97
|
-
public = fd._encode_point(fd._mul(a, fd._G))
|
|
98
|
-
r = int.from_bytes(hashlib.sha512(prefix + message).digest(), "little") % fd._L
|
|
99
|
-
big_r = fd._encode_point(fd._mul(r, fd._G))
|
|
100
|
-
k = int.from_bytes(hashlib.sha512(big_r + public + message).digest(), "little") % fd._L
|
|
101
|
-
return big_r + ((r + k * a) % fd._L).to_bytes(32, "little")
|
|
102
96
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
|
|
97
|
+
from fabric_crypto import ed25519_sign # noqa: E402,F401 - the core's: the bytes Node's crypto.sign gives
|
|
98
|
+
from fabric_crypto import b64url as _b64url # noqa: E402 - the core's (FAA-65)
|
|
106
99
|
|
|
107
100
|
|
|
108
101
|
def public_jwk(seed: bytes, kid: str) -> Dict[str, str]:
|
|
@@ -122,7 +115,7 @@ def read_signing_key(path: Any, kid: Optional[str] = None) -> Tuple[bytes, str]:
|
|
|
122
115
|
raise PackageError("Cannot read the key file %s: %s." % (path, exc.strerror)) from None
|
|
123
116
|
if os.name == "posix" and stat.st_mode & 0o077:
|
|
124
117
|
raise PackageError("The key file %s is readable by others (mode %o); chmod 600 it first." % (path, stat.st_mode & 0o777))
|
|
125
|
-
text = raw.decode("utf-8", "replace").strip()
|
|
118
|
+
text = raw.decode("utf-8", "replace").strip(JS_WHITESPACE) # as JavaScript's trim: a leading BOM goes too
|
|
126
119
|
if text.startswith("{"):
|
|
127
120
|
try:
|
|
128
121
|
jwk = json.loads(text)
|
|
@@ -132,7 +125,7 @@ def read_signing_key(path: Any, kid: Optional[str] = None) -> Tuple[bytes, str]:
|
|
|
132
125
|
raise PackageError("The key file must be an Ed25519 private JWK (kty OKP, crv Ed25519, with d).")
|
|
133
126
|
if jwk.get("alg", "EdDSA") != "EdDSA":
|
|
134
127
|
raise PackageError("The key names alg %r; a package signature is EdDSA (DEC-0051)." % jwk.get("alg"))
|
|
135
|
-
seed =
|
|
128
|
+
seed = _crypto.b64url_decode(jwk["d"]) if _B64URL.fullmatch(jwk["d"]) else b""
|
|
136
129
|
if len(seed) != 32:
|
|
137
130
|
raise PackageError("The key's d is not a 32-byte Ed25519 private key.")
|
|
138
131
|
if isinstance(jwk.get("x"), str) and jwk["x"] != _b64url(ed25519_public_key(seed)):
|
|
@@ -152,7 +145,7 @@ def read_signing_key(path: Any, kid: Optional[str] = None) -> Tuple[bytes, str]:
|
|
|
152
145
|
seed = der[16:]
|
|
153
146
|
else:
|
|
154
147
|
raise PackageError("The key file is neither a JWK nor a PEM private key.")
|
|
155
|
-
if not isinstance(kid, str) or not re.
|
|
148
|
+
if not isinstance(kid, str) or not re.fullmatch(r"[A-Za-z0-9._:-]{1,128}", kid):
|
|
156
149
|
raise PackageError("Name the key with --kid (1-128 of A-Z a-z 0-9 . _ : -): the installing machine pins it by that name.")
|
|
157
150
|
return seed, kid
|
|
158
151
|
|
|
@@ -168,16 +161,7 @@ def _schema() -> Dict[str, Any]:
|
|
|
168
161
|
return _SCHEMA_CACHE["s"]
|
|
169
162
|
|
|
170
163
|
|
|
171
|
-
|
|
172
|
-
if isinstance(a, bool) or isinstance(b, bool):
|
|
173
|
-
return isinstance(a, bool) and isinstance(b, bool) and a == b
|
|
174
|
-
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
|
|
175
|
-
return a == b
|
|
176
|
-
if isinstance(a, list) and isinstance(b, list):
|
|
177
|
-
return len(a) == len(b) and all(_json_equal(x, y) for x, y in zip(a, b))
|
|
178
|
-
if isinstance(a, dict) and isinstance(b, dict):
|
|
179
|
-
return a.keys() == b.keys() and all(_json_equal(a[k], b[k]) for k in a)
|
|
180
|
-
return type(a) is type(b) and a == b
|
|
164
|
+
from fabric_jsvalue import json_equal as _json_equal # noqa: E402 - the core's (FAA-65)
|
|
181
165
|
|
|
182
166
|
|
|
183
167
|
def _type_of(value: Any) -> str:
|
|
@@ -196,27 +180,67 @@ def _type_of(value: Any) -> str:
|
|
|
196
180
|
return "object"
|
|
197
181
|
|
|
198
182
|
|
|
199
|
-
|
|
200
|
-
|
|
183
|
+
# ASCII digits only and the whole string (fullmatch): Python's \d takes any Unicode digit and its $ a trailing newline,
|
|
184
|
+
# which JavaScript's do not (FAA-78, finding 43).
|
|
185
|
+
_DATE_TIME = re.compile(r"([0-9]{4})-([0-9]{2})-([0-9]{2})[Tt]([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]+)?([Zz]|[+-]([0-9]{2}):([0-9]{2}))")
|
|
186
|
+
_URI = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*:[^%s]*" % JS_WHITESPACE)
|
|
187
|
+
_MONTH_DAYS = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _leap(year: int) -> bool:
|
|
191
|
+
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
|
|
201
192
|
|
|
202
193
|
|
|
203
194
|
def _format_ok(fmt: str, value: Any) -> bool:
|
|
204
195
|
if not isinstance(value, str):
|
|
205
196
|
return True
|
|
206
197
|
if fmt == "date-time":
|
|
207
|
-
|
|
198
|
+
m = _DATE_TIME.fullmatch(value)
|
|
199
|
+
if not m:
|
|
208
200
|
return False
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
201
|
+
year, month, day, hour, minute, second = (int(g) for g in m.groups()[:6])
|
|
202
|
+
# The proleptic calendar the contract's ajv-formats checks: year 0000 exists and is a leap year (0000-02-29
|
|
203
|
+
# holds). datetime starts at year 1, so the days are counted here, as fabric-package.mjs counts them.
|
|
204
|
+
if not 1 <= month <= 12 or not 1 <= day <= (29 if month == 2 and _leap(year) else _MONTH_DAYS[month]):
|
|
205
|
+
return False
|
|
206
|
+
if hour > 23 or minute > 59 or second > 59:
|
|
212
207
|
return False
|
|
213
|
-
|
|
214
|
-
return offset in ("Z", "z") or (int(offset[1:3]) < 24 and int(offset[4:6]) < 60)
|
|
208
|
+
return m.group(8) in ("Z", "z") or (int(m.group(9)) < 24 and int(m.group(10)) < 60)
|
|
215
209
|
if fmt == "uri":
|
|
216
|
-
return bool(_URI.
|
|
210
|
+
return bool(_URI.fullmatch(value))
|
|
217
211
|
return True
|
|
218
212
|
|
|
219
213
|
|
|
214
|
+
_PATTERNS: Dict[str, Any] = {}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _js_pattern(source: str) -> Any:
|
|
218
|
+
"""A schema's ECMAScript pattern (``u`` flag) as Python reads it the same way: ``$`` is the end of the string, not
|
|
219
|
+
the end before a final newline; ``.`` stops at JavaScript's line terminators (\\n, \\r, U+2028, U+2029); ``\\d`` and
|
|
220
|
+
``\\w`` are ASCII. Escapes and character classes are copied as written."""
|
|
221
|
+
if source not in _PATTERNS:
|
|
222
|
+
out: List[str] = []
|
|
223
|
+
i, in_class = 0, False
|
|
224
|
+
while i < len(source):
|
|
225
|
+
ch = source[i]
|
|
226
|
+
if ch == "\\":
|
|
227
|
+
out.append(source[i:i + 2])
|
|
228
|
+
i += 2
|
|
229
|
+
continue
|
|
230
|
+
if in_class:
|
|
231
|
+
in_class = ch != "]"
|
|
232
|
+
elif ch == "[":
|
|
233
|
+
in_class = True
|
|
234
|
+
elif ch == "$":
|
|
235
|
+
ch = r"\Z"
|
|
236
|
+
elif ch == ".":
|
|
237
|
+
ch = _JS_DOT
|
|
238
|
+
out.append(ch)
|
|
239
|
+
i += 1
|
|
240
|
+
_PATTERNS[source] = re.compile("".join(out), re.ASCII)
|
|
241
|
+
return _PATTERNS[source]
|
|
242
|
+
|
|
243
|
+
|
|
220
244
|
def _pointer(token: Any) -> str:
|
|
221
245
|
return str(token).replace("~", "~0").replace("/", "~1")
|
|
222
246
|
|
|
@@ -249,7 +273,7 @@ def _validate(schema: Any, value: Any, at: str, out: List[Dict[str, str]]) -> No
|
|
|
249
273
|
out.append({"instancePath": at, "message": "is shorter than %d" % schema["minLength"]})
|
|
250
274
|
if "maxLength" in schema and length > schema["maxLength"]:
|
|
251
275
|
out.append({"instancePath": at, "message": "is longer than %d" % schema["maxLength"]})
|
|
252
|
-
if "pattern" in schema and not
|
|
276
|
+
if "pattern" in schema and not _js_pattern(schema["pattern"]).search(value):
|
|
253
277
|
out.append({"instancePath": at, "message": "does not match %s" % schema["pattern"]})
|
|
254
278
|
if "format" in schema and not _format_ok(schema["format"], value):
|
|
255
279
|
out.append({"instancePath": at, "message": "is not a %s" % schema["format"]})
|
|
@@ -373,7 +397,7 @@ def _whole(value: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
|
373
397
|
return out
|
|
374
398
|
|
|
375
399
|
|
|
376
|
-
_UNDEFINED
|
|
400
|
+
from fabric_jsvalue import MISSING as _UNDEFINED # noqa: E402 - the core's absent value (FAA-65)
|
|
377
401
|
|
|
378
402
|
|
|
379
403
|
def _same_member(obj: Dict[str, Any], name: str, other: Any) -> bool:
|
|
@@ -386,21 +410,9 @@ def _same_member(obj: Dict[str, Any], name: str, other: Any) -> bool:
|
|
|
386
410
|
return _json_equal(mine, other)
|
|
387
411
|
|
|
388
412
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
return "undefined"
|
|
393
|
-
if value is None:
|
|
394
|
-
return "null"
|
|
395
|
-
if isinstance(value, bool):
|
|
396
|
-
return "true" if value else "false"
|
|
397
|
-
if isinstance(value, (int, float)):
|
|
398
|
-
return fd.jcs(value)
|
|
399
|
-
if isinstance(value, list):
|
|
400
|
-
return ",".join("" if item is None else _js_string(item) for item in value)
|
|
401
|
-
if isinstance(value, dict):
|
|
402
|
-
return "[object Object]"
|
|
403
|
-
return value
|
|
413
|
+
from fabric_jsvalue import js_string as _js_string # noqa: E402 - the core's (FAA-65)
|
|
414
|
+
import fabric_jsvalue as _jsv # noqa: E402 - the core (FAA-65)
|
|
415
|
+
import fabric_crypto as _crypto # noqa: E402 - the core (FAA-65)
|
|
404
416
|
|
|
405
417
|
|
|
406
418
|
def _no_secret(value: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
@@ -481,6 +493,63 @@ def _mcp_roles(value: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
|
481
493
|
return out
|
|
482
494
|
|
|
483
495
|
|
|
496
|
+
# --- the path rule (FAA-78, finding 40; decision D5) ----------------------------------------------------------------
|
|
497
|
+
|
|
498
|
+
def _control(ch: str) -> bool:
|
|
499
|
+
"""C0 and C1 controls, DEL, and the two Unicode line terminators JavaScript's ``.`` stops at."""
|
|
500
|
+
code = ord(ch)
|
|
501
|
+
return code < 0x20 or 0x7F <= code <= 0x9F or code in (0x2028, 0x2029)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def path_problem(path: Any) -> Optional[str]:
|
|
505
|
+
"""Why ``path`` is no packaged path, or None. The schema's pattern says the same in one expression, but JavaScript's
|
|
506
|
+
``.`` stops at U+2028 and U+2029, so in Node and in the contract's own ajv its look-aheads never see a ``..``, a
|
|
507
|
+
backslash or a ``//`` after one (``a\u2028/../../etc/passwd`` passes there). This rule reads the path character by
|
|
508
|
+
character in both kits, and refuses C1 controls, DEL and the line terminators besides. A non-string is the
|
|
509
|
+
schema's to refuse."""
|
|
510
|
+
if not isinstance(path, str):
|
|
511
|
+
return None
|
|
512
|
+
if not 1 <= len(path) <= 256:
|
|
513
|
+
return "a path is 1 to 256 characters"
|
|
514
|
+
if any(_control(ch) for ch in path):
|
|
515
|
+
return "a path carries no control character and no line or paragraph separator"
|
|
516
|
+
if path[0] in "/~":
|
|
517
|
+
return "a path is relative to the package folder: it never starts with / or ~"
|
|
518
|
+
if "\\" in path or ":" in path:
|
|
519
|
+
return "a path is written with forward slashes and no colon"
|
|
520
|
+
if "//" in path or path.endswith("/"):
|
|
521
|
+
return "a path names a file: no empty segment and no trailing slash"
|
|
522
|
+
if any(segment in (".", "..") for segment in path.split("/")):
|
|
523
|
+
return "a path never steps through . or .."
|
|
524
|
+
return None
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _path_fields(pkg: Dict[str, Any]) -> List[Tuple[str, Any]]:
|
|
528
|
+
"""Every member the schema types as a path, by its JSON pointer."""
|
|
529
|
+
fields: List[Tuple[str, Any]] = [("/provider", pkg.get("provider", _UNDEFINED))]
|
|
530
|
+
evals = pkg.get("evals")
|
|
531
|
+
if isinstance(evals, dict):
|
|
532
|
+
fields.append(("/evals/suite", evals.get("suite", _UNDEFINED)))
|
|
533
|
+
for name in ("state", "dependencies", "files"):
|
|
534
|
+
items = pkg.get(name)
|
|
535
|
+
for index, entry in enumerate(items if isinstance(items, list) else []):
|
|
536
|
+
if isinstance(entry, dict):
|
|
537
|
+
fields.append(("/%s/%d/path" % (name, index), entry.get("path", _UNDEFINED)))
|
|
538
|
+
return [(at, value) for at, value in fields if value is not _UNDEFINED]
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def path_problems(pkg: Any) -> List[Dict[str, str]]:
|
|
542
|
+
"""The path rule over every path the package names: ``{instancePath, message}`` per path refused."""
|
|
543
|
+
if not isinstance(pkg, dict):
|
|
544
|
+
return []
|
|
545
|
+
out = []
|
|
546
|
+
for at, value in _path_fields(pkg):
|
|
547
|
+
why = path_problem(value)
|
|
548
|
+
if why is not None:
|
|
549
|
+
out.append({"instancePath": at, "message": why})
|
|
550
|
+
return out
|
|
551
|
+
|
|
552
|
+
|
|
484
553
|
def package_findings(pkg: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
485
554
|
"""FAC-SEM-064 (whole), 065 (no secret), 066 (agentRef derived) and 102 (one role, one server), in that order."""
|
|
486
555
|
return _whole(pkg) + _no_secret(pkg) + _agent_ref(pkg) + _mcp_roles(pkg)
|
|
@@ -492,8 +561,8 @@ def _parsed(data: Optional[bytes]) -> Optional[Dict[str, Any]]:
|
|
|
492
561
|
if data is None:
|
|
493
562
|
return None
|
|
494
563
|
try:
|
|
495
|
-
value = json.loads(data.decode("utf-8"
|
|
496
|
-
except ValueError:
|
|
564
|
+
value = json.loads(data.decode("utf-8"))
|
|
565
|
+
except (ValueError, RecursionError): # invalid UTF-8 is no manifest, in both kits
|
|
497
566
|
return None
|
|
498
567
|
return value if isinstance(value, dict) else None
|
|
499
568
|
|
|
@@ -537,13 +606,13 @@ def _key_for(keys: Any, kid: str, alg: str) -> Dict[str, Any]:
|
|
|
537
606
|
if jwk.get("alg", _UNDEFINED) != alg:
|
|
538
607
|
return {"problem": "key-algorithm-mismatch"}
|
|
539
608
|
x = jwk.get("x")
|
|
540
|
-
if (jwk.get("kty") != "OKP" or jwk.get("crv") != "Ed25519" or not isinstance(x, str) or not _B64URL.
|
|
609
|
+
if (jwk.get("kty") != "OKP" or jwk.get("crv") != "Ed25519" or not isinstance(x, str) or not _B64URL.fullmatch(x)
|
|
541
610
|
or "d" in jwk or ("use" in jwk and jwk["use"] != "sig")):
|
|
542
611
|
return {"problem": "unusable-key"}
|
|
543
|
-
raw =
|
|
612
|
+
raw = _crypto.b64url_decode(x)
|
|
544
613
|
if len(raw) != 32:
|
|
545
614
|
return {"problem": "unusable-key"}
|
|
546
|
-
if
|
|
615
|
+
if _crypto.is_small_order_key(raw): # DEC-0045: refused before any verification
|
|
547
616
|
return {"problem": "small-order-key"}
|
|
548
617
|
return {"key": raw}
|
|
549
618
|
|
|
@@ -570,7 +639,7 @@ def verify_package(pkg: Any, trust: Dict[str, Any], countersigned: bool = False)
|
|
|
570
639
|
trust = trust if isinstance(trust, dict) else {}
|
|
571
640
|
for index, entry in enumerate(signatures):
|
|
572
641
|
if (not isinstance(entry, dict) or entry.get("role") not in ("publisher", "organization") or not isinstance(entry.get("alg"), str)
|
|
573
|
-
or not isinstance(entry.get("kid"), str) or not isinstance(entry.get("sig"), str) or not _B64URL.
|
|
642
|
+
or not isinstance(entry.get("kid"), str) or not isinstance(entry.get("sig"), str) or not _B64URL.fullmatch(entry["sig"])):
|
|
574
643
|
return {"ok": False, "problem": "malformed", "signature": index}
|
|
575
644
|
if entry["alg"] not in PACKAGE_ALGORITHMS:
|
|
576
645
|
return {"ok": False, "problem": "unknown-algorithm", "signature": index}
|
|
@@ -580,8 +649,8 @@ def verify_package(pkg: Any, trust: Dict[str, Any], countersigned: bool = False)
|
|
|
580
649
|
continue
|
|
581
650
|
if "problem" in found:
|
|
582
651
|
return {"ok": False, "problem": found["problem"], "signature": index}
|
|
583
|
-
signature =
|
|
584
|
-
if len(signature) != 64 or not
|
|
652
|
+
signature = _crypto.b64url_decode(entry["sig"])
|
|
653
|
+
if len(signature) != 64 or not _crypto.ed25519_verify(found["key"], package_signing_input(digest, entry["role"]), signature):
|
|
585
654
|
return {"ok": False, "problem": "bad-signature", "signature": index}
|
|
586
655
|
signers.append({"role": entry["role"], "kid": entry["kid"]})
|
|
587
656
|
if countersigned is True and not any(s["role"] == "organization" for s in signers):
|
|
@@ -614,50 +683,142 @@ def consumer_placement(pkg: Any, configured: Any) -> List[Dict[str, str]]:
|
|
|
614
683
|
# --- the folder on disk -----------------------------------------------------------------------------------
|
|
615
684
|
|
|
616
685
|
def _litter(name: str) -> Optional[str]:
|
|
617
|
-
if JUNK.
|
|
686
|
+
if JUNK.fullmatch(name):
|
|
618
687
|
return "litter"
|
|
619
|
-
if SECRET_FILE.
|
|
688
|
+
if SECRET_FILE.fullmatch(name) and not SECRET_FILE_OK.fullmatch(name):
|
|
620
689
|
return "a credential file"
|
|
621
690
|
return None
|
|
622
691
|
|
|
623
692
|
|
|
693
|
+
#: Opening a packaged file: never through a link, never waiting on a pipe (FAA-78, finding 42). Folders are opened
|
|
694
|
+
#: without following a link too, and what is in one is opened relative to it, so a folder swapped for a link mid-walk is
|
|
695
|
+
#: never followed either; where the platform has no such calls (Windows), the walk goes by path.
|
|
696
|
+
_NOFOLLOW = getattr(os, "O_NOFOLLOW", 0)
|
|
697
|
+
_FILE_FLAGS = os.O_RDONLY | _NOFOLLOW | getattr(os, "O_NONBLOCK", 0) | getattr(os, "O_BINARY", 0)
|
|
698
|
+
_DIR_FLAGS = os.O_RDONLY | _NOFOLLOW | getattr(os, "O_DIRECTORY", 0)
|
|
699
|
+
_LINK_ERRNOS = (errno.ELOOP, errno.EMLINK) # open() on a link it may not follow: ELOOP (Linux, macOS), EMLINK (FreeBSD)
|
|
700
|
+
_BY_FOLDER = bool(_NOFOLLOW) and hasattr(os, "O_DIRECTORY") and os.open in os.supports_dir_fd and os.scandir in getattr(os, "supports_fd", set())
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def _unreadable(rel: str, exc: OSError) -> PackageError:
|
|
704
|
+
"""``Cannot read <path>: EACCES.``: the error's symbolic name, as Node's error.code names it."""
|
|
705
|
+
return PackageError("Cannot read %s: %s." % (rel or ".", errno.errorcode.get(exc.errno or -1) or exc.strerror or exc))
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def _read_file(name: str, dir_fd: Optional[int]) -> Tuple[Optional[bytes], Optional[str]]:
|
|
709
|
+
"""``(bytes, None)`` for a regular file, ``(None, why)`` for a link or anything else. Raises OSError."""
|
|
710
|
+
try:
|
|
711
|
+
handle = os.open(name, _FILE_FLAGS, dir_fd=dir_fd)
|
|
712
|
+
except OSError as exc:
|
|
713
|
+
if _NOFOLLOW and exc.errno in _LINK_ERRNOS:
|
|
714
|
+
return None, "a symlink"
|
|
715
|
+
raise
|
|
716
|
+
try:
|
|
717
|
+
if not _stat.S_ISREG(os.fstat(handle).st_mode):
|
|
718
|
+
return None, "not a regular file"
|
|
719
|
+
chunks = []
|
|
720
|
+
while True:
|
|
721
|
+
chunk = os.read(handle, 1 << 20)
|
|
722
|
+
if not chunk:
|
|
723
|
+
return b"".join(chunks), None
|
|
724
|
+
chunks.append(chunk)
|
|
725
|
+
finally:
|
|
726
|
+
os.close(handle)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _walk(here: Any, rel: str, files: Dict[str, bytes], refused: List[str]) -> None:
|
|
730
|
+
"""One folder: ``here`` is its descriptor when the walk goes by folder, else its path."""
|
|
731
|
+
by_fd = isinstance(here, int)
|
|
732
|
+
try:
|
|
733
|
+
with os.scandir(here) as listing:
|
|
734
|
+
entries = sorted(listing, key=lambda e: e.name)
|
|
735
|
+
except OSError as exc:
|
|
736
|
+
raise _unreadable(rel, exc) from None
|
|
737
|
+
for entry in entries:
|
|
738
|
+
where = "%s/%s" % (rel, entry.name) if rel else entry.name
|
|
739
|
+
try:
|
|
740
|
+
why = "a symlink" if entry.is_symlink() else _litter(entry.name)
|
|
741
|
+
folder = entry.is_dir(follow_symlinks=False)
|
|
742
|
+
if why is None and not folder and not entry.is_file(follow_symlinks=False):
|
|
743
|
+
why = "not a regular file"
|
|
744
|
+
if why is None and folder:
|
|
745
|
+
inner = os.open(entry.name, _DIR_FLAGS, dir_fd=here) if by_fd else entry.path
|
|
746
|
+
try:
|
|
747
|
+
_walk(inner, where, files, refused)
|
|
748
|
+
finally:
|
|
749
|
+
if by_fd:
|
|
750
|
+
os.close(inner)
|
|
751
|
+
continue
|
|
752
|
+
if why is None:
|
|
753
|
+
data, why = _read_file(entry.name if by_fd else entry.path, here if by_fd else None)
|
|
754
|
+
if why is None:
|
|
755
|
+
files[where] = data # type: ignore[assignment]
|
|
756
|
+
continue
|
|
757
|
+
except OSError as exc:
|
|
758
|
+
if not (_NOFOLLOW and exc.errno in _LINK_ERRNOS):
|
|
759
|
+
raise _unreadable(where, exc) from None
|
|
760
|
+
why = "a symlink"
|
|
761
|
+
refused.append("%s (%s)" % (where, why))
|
|
762
|
+
|
|
763
|
+
|
|
624
764
|
def read_folder(folder: Any) -> Dict[str, bytes]:
|
|
625
765
|
"""Every regular file under ``folder`` by its forward-slash path. Refuses a symlink, a special file, version-control
|
|
626
|
-
or cache litter, and a file that is a credential by its name
|
|
627
|
-
clean copy.
|
|
766
|
+
or cache litter, and a file that is a credential by its name, each named, so the person can remove it or pack a
|
|
767
|
+
clean copy. A file or folder it cannot read is a refusal naming it (``Cannot read <path>: EACCES.``), never
|
|
768
|
+
skipped (FAA-78, finding 41); a file is opened without following a link and read only when it is a regular file
|
|
769
|
+
(finding 42)."""
|
|
628
770
|
root = Path(folder)
|
|
629
771
|
if not root.is_dir():
|
|
630
772
|
raise PackageError("%s is not a folder." % root)
|
|
631
773
|
files: Dict[str, bytes] = {}
|
|
632
774
|
refused: List[str] = []
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
for name in filenames:
|
|
643
|
-
rel = name if rel_dir == "." else "%s/%s" % (rel_dir, name)
|
|
644
|
-
full = here / name
|
|
645
|
-
why = "a symlink" if full.is_symlink() else (None if full.is_file() else "not a regular file") or _litter(name)
|
|
646
|
-
if why:
|
|
647
|
-
refused.append("%s (%s)" % (rel, why))
|
|
648
|
-
continue
|
|
649
|
-
files[rel] = full.read_bytes()
|
|
775
|
+
try:
|
|
776
|
+
top = os.open(str(root), os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)) if _BY_FOLDER else str(root)
|
|
777
|
+
except OSError as exc:
|
|
778
|
+
raise _unreadable(str(root), exc) from None
|
|
779
|
+
try:
|
|
780
|
+
_walk(top, "", files, refused)
|
|
781
|
+
finally:
|
|
782
|
+
if _BY_FOLDER:
|
|
783
|
+
os.close(top) # type: ignore[arg-type]
|
|
650
784
|
if refused:
|
|
651
785
|
raise PackageError("Not packed: remove these or pack a clean copy: " + "; ".join(sorted(refused)))
|
|
652
786
|
return files
|
|
653
787
|
|
|
654
788
|
|
|
789
|
+
def _nesting(text: str) -> int:
|
|
790
|
+
"""How deep the arrays and objects of a JSON text nest, read without recursion (strings skipped)."""
|
|
791
|
+
depth = deepest = 0
|
|
792
|
+
i, end = 0, len(text)
|
|
793
|
+
while i < end:
|
|
794
|
+
ch = text[i]
|
|
795
|
+
if ch == '"':
|
|
796
|
+
i += 1
|
|
797
|
+
while i < end and text[i] != '"':
|
|
798
|
+
i += 2 if text[i] == "\\" else 1
|
|
799
|
+
elif ch in "[{":
|
|
800
|
+
depth += 1
|
|
801
|
+
deepest = max(deepest, depth)
|
|
802
|
+
elif ch in "]}":
|
|
803
|
+
depth -= 1
|
|
804
|
+
i += 1
|
|
805
|
+
return deepest
|
|
806
|
+
|
|
807
|
+
|
|
655
808
|
def _load_json(data: bytes, what: str) -> Dict[str, Any]:
|
|
809
|
+
"""A JSON object from strict UTF-8 bytes: no member named twice, nesting at most MAX_NESTING deep. Every refusal
|
|
810
|
+
is a PackageError, never a traceback (a RecursionError included)."""
|
|
811
|
+
try:
|
|
812
|
+
text = data.decode("utf-8")
|
|
813
|
+
except UnicodeDecodeError as exc:
|
|
814
|
+
raise PackageError("%s is not JSON: %s" % (what, exc)) from None
|
|
815
|
+
if _nesting(text) > MAX_NESTING:
|
|
816
|
+
raise PackageError("%s nests deeper than %d levels." % (what, MAX_NESTING))
|
|
656
817
|
try:
|
|
657
|
-
value = json.loads(
|
|
818
|
+
value = json.loads(text, object_pairs_hook=fd._no_duplicates)
|
|
658
819
|
except fd._Duplicate as exc:
|
|
659
820
|
raise PackageError("%s names %s twice in one object." % (what, json.dumps(exc.args[0], ensure_ascii=False))) from None
|
|
660
|
-
except (ValueError,
|
|
821
|
+
except (ValueError, RecursionError) as exc:
|
|
661
822
|
raise PackageError("%s is not JSON: %s" % (what, exc)) from None
|
|
662
823
|
if not isinstance(value, dict):
|
|
663
824
|
raise PackageError("%s is not a JSON object." % what)
|
|
@@ -670,12 +831,12 @@ def dumps(value: Any, indent: str = "") -> str:
|
|
|
670
831
|
if isinstance(value, dict):
|
|
671
832
|
if not value:
|
|
672
833
|
return "{}"
|
|
673
|
-
return "{\n" + ",\n".join(inner +
|
|
834
|
+
return "{\n" + ",\n".join(inner + _jsv.json_string_literal(k) + ": " + dumps(v, inner) for k, v in value.items()) + "\n" + indent + "}"
|
|
674
835
|
if isinstance(value, list):
|
|
675
836
|
if not value:
|
|
676
837
|
return "[]"
|
|
677
838
|
return "[\n" + ",\n".join(inner + dumps(v, inner) for v in value) + "\n" + indent + "]"
|
|
678
|
-
return
|
|
839
|
+
return _jsv.jcs(value)
|
|
679
840
|
|
|
680
841
|
|
|
681
842
|
def compose(draft: Dict[str, Any], files: Dict[str, bytes]) -> Tuple[Dict[str, Any], List[str]]:
|
|
@@ -699,12 +860,16 @@ def compose(draft: Dict[str, Any], files: Dict[str, bytes]) -> Tuple[Dict[str, A
|
|
|
699
860
|
notes.append("dropped %d signature(s): a repacked package is signed again" % len(draft["signatures"]))
|
|
700
861
|
pkg["files"] = [{"path": p, "sha256": sha256_of(files[p])} for p in sorted((p for p in files if p != PACKAGE_FILE),
|
|
701
862
|
key=lambda p: p.encode("utf-16-be", "surrogatepass"))]
|
|
702
|
-
|
|
863
|
+
try:
|
|
864
|
+
pkg["digest"] = package_digest(pkg)
|
|
865
|
+
except TypeError as exc:
|
|
866
|
+
raise PackageError("%s has no RFC 8785 form, so it has no digest: %s" % (PACKAGE_FILE, exc)) from None
|
|
703
867
|
return pkg, notes
|
|
704
868
|
|
|
705
869
|
|
|
706
870
|
def _all_problems(pkg: Dict[str, Any], files: Dict[str, bytes]) -> List[Dict[str, str]]:
|
|
707
871
|
problems = [dict(p, rule="schema") for p in schema_problems(pkg)]
|
|
872
|
+
problems += [dict(p, rule="path") for p in path_problems(pkg)]
|
|
708
873
|
problems += [dict(p, rule=p["code"]) for p in package_findings(pkg)]
|
|
709
874
|
problems += [{"rule": "folder", "problem": p["problem"], "instancePath": p["path"], "message": p["problem"]} for p in check_package_folder(pkg, files)]
|
|
710
875
|
return problems
|
|
@@ -735,9 +900,9 @@ def pack(folder: Any, apply: bool = False) -> Dict[str, Any]:
|
|
|
735
900
|
|
|
736
901
|
|
|
737
902
|
def fd_write(path: Path, text: str) -> None:
|
|
738
|
-
"""Write through a temporary file
|
|
903
|
+
"""Write through a temporary file and rename it over; bytes, as the Node kit writes them (text mode: \r\n on Windows)."""
|
|
739
904
|
temp = path.with_name(".%s.tmp-%d" % (path.name, os.getpid()))
|
|
740
|
-
temp.
|
|
905
|
+
temp.write_bytes(text.encode("utf-8"))
|
|
741
906
|
os.replace(temp, path)
|
|
742
907
|
|
|
743
908
|
|
|
@@ -783,6 +948,17 @@ def verify(folder: Any, trust: Dict[str, Any], countersigned: bool = False) -> D
|
|
|
783
948
|
return {"ok": bool(verdict.get("ok")) and not problems, "verdict": verdict, "problems": problems}
|
|
784
949
|
|
|
785
950
|
|
|
951
|
+
def _emit(text: str) -> None:
|
|
952
|
+
"""One answer line, UTF-8 and \\n as the Node kit writes it, whatever the locale (a Windows pipe is cp1252; FAA-72)."""
|
|
953
|
+
buffer = getattr(sys.stdout, "buffer", None)
|
|
954
|
+
if buffer is None: # a stream a caller put in place takes text
|
|
955
|
+
sys.stdout.write(text + "\n")
|
|
956
|
+
return
|
|
957
|
+
sys.stdout.flush()
|
|
958
|
+
buffer.write((text + "\n").encode("utf-8"))
|
|
959
|
+
buffer.flush()
|
|
960
|
+
|
|
961
|
+
|
|
786
962
|
def main(argv: Optional[List[str]] = None) -> int:
|
|
787
963
|
parser = argparse.ArgumentParser(prog="fabric_package.py", description="Pack, sign and verify a fabric-package/1 folder.")
|
|
788
964
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
@@ -814,9 +990,9 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
814
990
|
trust = _load_json(Path(args.trust).read_bytes(), args.trust)
|
|
815
991
|
answer = verify(args.folder, trust, countersigned=args.countersigned)
|
|
816
992
|
except (PackageError, OSError) as exc:
|
|
817
|
-
|
|
993
|
+
_emit(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False, separators=(",", ":")))
|
|
818
994
|
return 1
|
|
819
|
-
|
|
995
|
+
_emit(dumps(answer))
|
|
820
996
|
return 0 if answer.get("ok") else 1
|
|
821
997
|
|
|
822
998
|
|