@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_delegation.py
CHANGED
|
@@ -16,8 +16,8 @@ R compared by its encoding).
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
-
import base64
|
|
20
19
|
import hashlib
|
|
20
|
+
import http.client
|
|
21
21
|
import ipaddress
|
|
22
22
|
import json
|
|
23
23
|
import math
|
|
@@ -31,6 +31,21 @@ from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple
|
|
|
31
31
|
import urllib.error
|
|
32
32
|
import urllib.parse
|
|
33
33
|
import urllib.request
|
|
34
|
+
import sys as _sys # noqa: E402
|
|
35
|
+
from pathlib import Path as _Path # noqa: E402
|
|
36
|
+
if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from any working directory (FAA-65)
|
|
37
|
+
_sys.path.insert(0, str(_Path(__file__).resolve().parent))
|
|
38
|
+
from fabric_crypto import ( # noqa: E402 - the core (FAA-65)
|
|
39
|
+
SMALL_ORDER_ENCODINGS, # noqa: F401 - public here since v0.39.1 (test/fixtures/kit-api)
|
|
40
|
+
b64url as _b64encode,
|
|
41
|
+
b64url_decode as _b64decode,
|
|
42
|
+
ed25519_verify,
|
|
43
|
+
is_small_order_key,
|
|
44
|
+
)
|
|
45
|
+
from fabric_jsvalue import ( # noqa: E402 - the core (FAA-65)
|
|
46
|
+
MISSING as _MISSING,
|
|
47
|
+
jcs,
|
|
48
|
+
)
|
|
34
49
|
|
|
35
50
|
REFUSAL_CODES = (
|
|
36
51
|
"unauthenticated", "token-expired", "wrong-audience", "scope-not-granted", "effect-above-ceiling", "no-grant",
|
|
@@ -50,6 +65,8 @@ JWKS_MAX_AGE_SECONDS = 600
|
|
|
50
65
|
JWKS_REREAD_SECONDS = 60
|
|
51
66
|
# After a failed read, wait this long before trying again, so a down issuer does not cost every call a fetch.
|
|
52
67
|
JWKS_RETRY_SECONDS = 10
|
|
68
|
+
# One read of the key set, headers and body together, ends within this many seconds (Node's AbortSignal.timeout(5000)).
|
|
69
|
+
JWKS_FETCH_SECONDS = 5.0
|
|
53
70
|
# JavaScript's String.prototype.trim set, so a header is trimmed as the contract's verifier trims it.
|
|
54
71
|
_JS_WHITESPACE = "\t\n\v\f\r \u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
|
|
55
72
|
|
|
@@ -76,31 +93,15 @@ REFUSAL_MESSAGES = {
|
|
|
76
93
|
"unknown-state": "The state cannot be established, so the call is refused.",
|
|
77
94
|
}
|
|
78
95
|
|
|
79
|
-
_B64URL = re.compile(r"^[A-Za-z0-9_-]
|
|
96
|
+
_B64URL = re.compile(r"^[A-Za-z0-9_-]+\Z")
|
|
80
97
|
# DL-2's grammar, as the contract's verifier types the claims (DEC-0045): an agent id, an opaque session or task id,
|
|
81
98
|
# a URI, a jti and a scope.
|
|
82
|
-
AGENT_ID = re.compile(r"^[a-z][a-z0-9-]{1,62}(\.[a-z][a-z0-9-]{0,31})
|
|
83
|
-
OPAQUE_ID = re.compile(r"^[A-Za-z0-9._:-]{1,128}
|
|
84
|
-
_URI = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:\S
|
|
85
|
-
_JTI = re.compile(r"^[A-Za-z0-9._~-]{16,128}
|
|
86
|
-
_SCOPE = re.compile(r"^[a-z][a-z0-9._-]{1,127}( [a-z][a-z0-9._-]{1,127})
|
|
99
|
+
AGENT_ID = re.compile(r"^[a-z][a-z0-9-]{1,62}(\.[a-z][a-z0-9-]{0,31})?\Z")
|
|
100
|
+
OPAQUE_ID = re.compile(r"^[A-Za-z0-9._:-]{1,128}\Z")
|
|
101
|
+
_URI = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:\S+\Z")
|
|
102
|
+
_JTI = re.compile(r"^[A-Za-z0-9._~-]{16,128}\Z")
|
|
103
|
+
_SCOPE = re.compile(r"^[a-z][a-z0-9._-]{1,127}( [a-z][a-z0-9._-]{1,127})*\Z")
|
|
87
104
|
_SPEND_WINDOWS = ("total", "task", "project", "day", "week", "month")
|
|
88
|
-
# The small-order points of edwards25519 and their non-canonical encodings, sign bit cleared (the contract's
|
|
89
|
-
# src/ed25519.ts; libsodium's has_small_order list).
|
|
90
|
-
SMALL_ORDER_ENCODINGS = (
|
|
91
|
-
"0000000000000000000000000000000000000000000000000000000000000000", "0100000000000000000000000000000000000000000000000000000000000000",
|
|
92
|
-
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05", "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
|
|
93
|
-
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
|
94
|
-
"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
|
95
|
-
)
|
|
96
|
-
_MISSING = object()
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def is_small_order_key(raw: bytes) -> bool:
|
|
100
|
-
"""Whether a 32-byte Ed25519 public key is a point of small order, in any encoding (CT-29)."""
|
|
101
|
-
if len(raw) != 32:
|
|
102
|
-
return True
|
|
103
|
-
return (raw[:31] + bytes([raw[31] & 0x7F])).hex() in SMALL_ORDER_ENCODINGS
|
|
104
105
|
|
|
105
106
|
|
|
106
107
|
def _full(pattern: "re.Pattern[str]", value: Any) -> bool:
|
|
@@ -128,90 +129,9 @@ def _is_time(value: Any) -> bool:
|
|
|
128
129
|
|
|
129
130
|
# --- Ed25519 (RFC 8032 §5.1) ----------------------------------------------------------------------------
|
|
130
131
|
|
|
131
|
-
_P = 2 ** 255 - 19
|
|
132
|
-
_L = 2 ** 252 + 27742317777372353535851937790883648493
|
|
133
|
-
_D = (-121665 * pow(121666, _P - 2, _P)) % _P
|
|
134
|
-
_SQRT_M1 = pow(2, (_P - 1) // 4, _P)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def _add(a: Tuple[int, int, int, int], b: Tuple[int, int, int, int]) -> Tuple[int, int, int, int]:
|
|
138
|
-
x1, y1, z1, t1 = a
|
|
139
|
-
x2, y2, z2, t2 = b
|
|
140
|
-
aa = (y1 - x1) * (y2 - x2) % _P
|
|
141
|
-
bb = (y1 + x1) * (y2 + x2) % _P
|
|
142
|
-
cc = 2 * t1 * t2 * _D % _P
|
|
143
|
-
dd = 2 * z1 * z2 % _P
|
|
144
|
-
e, f, g, h = bb - aa, dd - cc, dd + cc, bb + aa
|
|
145
|
-
return (e * f % _P, g * h % _P, f * g % _P, e * h % _P)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def _mul(scalar: int, point: Tuple[int, int, int, int]) -> Tuple[int, int, int, int]:
|
|
149
|
-
result = (0, 1, 1, 0)
|
|
150
|
-
while scalar:
|
|
151
|
-
if scalar & 1:
|
|
152
|
-
result = _add(result, point)
|
|
153
|
-
point = _add(point, point)
|
|
154
|
-
scalar >>= 1
|
|
155
|
-
return result
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def _decode_point(data: bytes) -> Optional[Tuple[int, int, int, int]]:
|
|
159
|
-
if len(data) != 32:
|
|
160
|
-
return None
|
|
161
|
-
y = int.from_bytes(data, "little")
|
|
162
|
-
sign = y >> 255
|
|
163
|
-
y &= (1 << 255) - 1
|
|
164
|
-
if y >= _P:
|
|
165
|
-
return None
|
|
166
|
-
x2 = (y * y - 1) * pow(_D * y * y + 1, _P - 2, _P) % _P
|
|
167
|
-
if x2 == 0:
|
|
168
|
-
if sign:
|
|
169
|
-
return None
|
|
170
|
-
return (0, y, 1, 0)
|
|
171
|
-
x = pow(x2, (_P + 3) // 8, _P)
|
|
172
|
-
if (x * x - x2) % _P:
|
|
173
|
-
x = x * _SQRT_M1 % _P
|
|
174
|
-
if (x * x - x2) % _P:
|
|
175
|
-
return None
|
|
176
|
-
if x & 1 != sign:
|
|
177
|
-
x = _P - x
|
|
178
|
-
return (x, y, 1, x * y % _P)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def _encode_point(point: Tuple[int, int, int, int]) -> bytes:
|
|
182
|
-
x, y, z, _ = point
|
|
183
|
-
zi = pow(z, _P - 2, _P)
|
|
184
|
-
x, y = x * zi % _P, y * zi % _P
|
|
185
|
-
return (y | ((x & 1) << 255)).to_bytes(32, "little")
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
_G = _decode_point(bytes.fromhex("5866666666666666666666666666666666666666666666666666666666666666"))
|
|
189
|
-
_IDENTITY = (1).to_bytes(32, "little")
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def ed25519_verify(public_key: bytes, message: bytes, signature: bytes) -> bool:
|
|
193
|
-
"""RFC 8032 §5.1.7, cofactorless: [S]B == R + [k]A, compared by R's encoding. False, never an exception."""
|
|
194
|
-
if len(public_key) != 32 or len(signature) != 64:
|
|
195
|
-
return False
|
|
196
|
-
s = int.from_bytes(signature[32:], "little")
|
|
197
|
-
if s >= _L:
|
|
198
|
-
return False
|
|
199
|
-
a = _decode_point(public_key)
|
|
200
|
-
if a is None or _encode_point(_mul(8, a)) == _IDENTITY:
|
|
201
|
-
return False # a small-order key would accept R = [S]B for a quarter or more of all messages; OpenSSL refuses it
|
|
202
|
-
k = int.from_bytes(hashlib.sha512(signature[:32] + public_key + message).digest(), "little") % _L
|
|
203
|
-
neg_a = ((-a[0]) % _P, a[1], a[2], (-a[3]) % _P)
|
|
204
|
-
return secrets.compare_digest(_encode_point(_add(_mul(s, _G), _mul(k, neg_a))), signature[:32])
|
|
205
|
-
|
|
206
132
|
|
|
207
133
|
# --- the compact document ------------------------------------------------------------------------------
|
|
208
134
|
|
|
209
|
-
def _b64decode(segment: str) -> bytes:
|
|
210
|
-
"""Base64url without padding, as Node reads it: a dangling sixth bit group is ignored."""
|
|
211
|
-
if len(segment) % 4 == 1:
|
|
212
|
-
segment = segment[:-1]
|
|
213
|
-
return base64.urlsafe_b64decode(segment + "=" * (-len(segment) % 4))
|
|
214
|
-
|
|
215
135
|
|
|
216
136
|
class _Duplicate(ValueError):
|
|
217
137
|
pass
|
|
@@ -305,7 +225,8 @@ def _key_for(jwks: Dict[str, Any], kid: str) -> Dict[str, Any]:
|
|
|
305
225
|
raw = _b64decode(jwk["x"])
|
|
306
226
|
except ValueError:
|
|
307
227
|
return {"problem": "unusable-key"}
|
|
308
|
-
|
|
228
|
+
# D4: exactly the 43 characters that write 32 bytes, so the key compared with the estate record is one string.
|
|
229
|
+
if len(raw) != 32 or _b64encode(raw) != jwk["x"]:
|
|
309
230
|
return {"problem": "unusable-key"}
|
|
310
231
|
if is_small_order_key(raw):
|
|
311
232
|
return {"problem": "small-order-key"} # refused before any verification (DEC-0045, CT-29)
|
|
@@ -460,24 +381,27 @@ def _readable_clock(now: Any) -> bool:
|
|
|
460
381
|
return False
|
|
461
382
|
|
|
462
383
|
|
|
463
|
-
def _estate_problem(estate: Any, kid: str, x: str, now: float, applied: Any =
|
|
464
|
-
revocations: Any =
|
|
384
|
+
def _estate_problem(estate: Any, kid: str, x: str, now: float, applied: Any = _MISSING,
|
|
385
|
+
revocations: Any = _MISSING) -> Optional[Tuple[str, str]]:
|
|
465
386
|
"""DL-14 row 4a (DEC-0055, DEC-0060), right after the signature: when the callee reads the estate record, the
|
|
466
387
|
token's kid and the published key must be certified by that revision. `estate` is a revision the caller verified
|
|
467
|
-
(fabric_estate.verify_estate_record) and its machine applied, or a callable returning it;
|
|
468
|
-
|
|
469
|
-
(`{"revision", "digest"?}`); `revocations` the revocation
|
|
470
|
-
|
|
471
|
-
|
|
388
|
+
(fabric_estate.verify_estate_record) and its machine applied, or a callable returning it; only an omitted `estate`
|
|
389
|
+
skips the rule, and None (given, or returned by the callable) is an unreadable source, as Node's null is (D3).
|
|
390
|
+
`applied` is the highest revision the machine applied (`{"revision", "digest"?}`); `revocations` the revocation
|
|
391
|
+
delta's entries applied since (a list or a callable). An omitted one is absent; None is unreadable. A source that
|
|
392
|
+
raises is unreadable, never a pass. Returns ("token" | "issuer", detail) or None."""
|
|
393
|
+
if estate is _MISSING:
|
|
472
394
|
return None
|
|
473
395
|
try:
|
|
474
396
|
record = estate() if callable(estate) else estate
|
|
475
397
|
extra = revocations() if callable(revocations) else revocations
|
|
476
398
|
except Exception:
|
|
477
399
|
return ("issuer", "estate-unreadable")
|
|
400
|
+
if applied is None: # the contract's hubKeyCertified refuses a null applied mark after its record checks: unreadable
|
|
401
|
+
return ("issuer", "estate-unreadable")
|
|
478
402
|
import fabric_estate # beside this file; imported here, since fabric_estate imports this module
|
|
479
|
-
|
|
480
|
-
|
|
403
|
+
given = {} if applied is _MISSING else {"applied": applied}
|
|
404
|
+
verdict = fabric_estate.hub_key_certified(record, kid, x, now, extra_revocations=extra, **given)
|
|
481
405
|
if verdict["certified"]:
|
|
482
406
|
return None
|
|
483
407
|
# The clock was refused before row 1, so a verdict here is the token's or the trust source's.
|
|
@@ -485,8 +409,8 @@ def _estate_problem(estate: Any, kid: str, x: str, now: float, applied: Any = No
|
|
|
485
409
|
|
|
486
410
|
|
|
487
411
|
def verify_delegation(token: Any, *, audience: str, jwks: Any, now: float, capability: str, effect: str,
|
|
488
|
-
issuer: Optional[str], read_only: bool = False, estate: Any =
|
|
489
|
-
estate_applied: Any =
|
|
412
|
+
issuer: Optional[str], read_only: bool = False, estate: Any = _MISSING,
|
|
413
|
+
estate_applied: Any = _MISSING, estate_revocations: Any = _MISSING) -> Dict[str, Any]:
|
|
490
414
|
"""DL-13, in its fixed order: nothing about a token is believed before its signature is.
|
|
491
415
|
`now` is seconds since the epoch. Returns {ok: True, claims, principal, caller, chain, kid} or a refusal
|
|
492
416
|
{ok: False, code, detail[, data]} whose detail is a short machine word, never the token. `issuer` is required.
|
|
@@ -606,8 +530,8 @@ UNSAFE_NO_JTI_STORE = _NoReplayProtection()
|
|
|
606
530
|
|
|
607
531
|
|
|
608
532
|
def verify_approval(approval: Optional[str], *, audience: str, jwks: Any, now: float, action_digest: str,
|
|
609
|
-
jti_store: Any, issuer: Optional[str], estate: Any =
|
|
610
|
-
estate_revocations: Any =
|
|
533
|
+
jti_store: Any, issuer: Optional[str], estate: Any = _MISSING, estate_applied: Any = _MISSING,
|
|
534
|
+
estate_revocations: Any = _MISSING) -> Dict[str, Any]:
|
|
611
535
|
"""DL-18: a missing approval is approval-required carrying the action digest; any fault of the document is
|
|
612
536
|
approval-invalid; a second use is approval-reused. The jti is consumed last."""
|
|
613
537
|
if approval is None or approval == "":
|
|
@@ -681,90 +605,6 @@ def read_delegation_token(headers: Any) -> Optional[Dict[str, str]]:
|
|
|
681
605
|
|
|
682
606
|
# --- RFC 8785 canonical JSON --------------------------------------------------------------------------------
|
|
683
607
|
|
|
684
|
-
def _number(value: float) -> str:
|
|
685
|
-
"""ECMAScript Number::toString, which RFC 8785 §3.2.2.3 prescribes."""
|
|
686
|
-
if value == 0:
|
|
687
|
-
return "0"
|
|
688
|
-
if value < 0:
|
|
689
|
-
return "-" + _number(-value)
|
|
690
|
-
digits, exponent = _shortest(value)
|
|
691
|
-
k, n = len(digits), len(digits) + exponent
|
|
692
|
-
if k <= n <= 21:
|
|
693
|
-
return digits + "0" * (n - k)
|
|
694
|
-
if 0 < n <= 21:
|
|
695
|
-
return digits[:n] + "." + digits[n:]
|
|
696
|
-
if -6 < n <= 0:
|
|
697
|
-
return "0." + "0" * (-n) + digits
|
|
698
|
-
e = n - 1
|
|
699
|
-
mantissa = digits if k == 1 else digits[0] + "." + digits[1:]
|
|
700
|
-
return "%se%s%d" % (mantissa, "+" if e >= 0 else "-", abs(e))
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
def _shortest(value: float) -> Tuple[str, int]:
|
|
704
|
-
"""The shortest round-trip digits of a positive finite double (Python's repr is shortest round-trip)."""
|
|
705
|
-
text = repr(value)
|
|
706
|
-
mantissa, _, exp = text.partition("e")
|
|
707
|
-
whole, _, frac = mantissa.partition(".")
|
|
708
|
-
digits = (whole + frac).lstrip("0")
|
|
709
|
-
exponent = (int(exp) if exp else 0) - len(frac)
|
|
710
|
-
if not digits:
|
|
711
|
-
return "0", 0
|
|
712
|
-
stripped = digits.rstrip("0")
|
|
713
|
-
exponent += len(digits) - len(stripped)
|
|
714
|
-
return stripped, exponent
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
_ESCAPES = {'"': '\\"', "\\": "\\\\", "\b": "\\b", "\f": "\\f", "\n": "\\n", "\r": "\\r", "\t": "\\t"}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
def _string(value: str) -> str:
|
|
721
|
-
out = ['"']
|
|
722
|
-
for ch in value:
|
|
723
|
-
code = ord(ch)
|
|
724
|
-
if 0xD800 <= code <= 0xDFFF:
|
|
725
|
-
raise TypeError("a lone surrogate has no canonical form")
|
|
726
|
-
if ch in _ESCAPES:
|
|
727
|
-
out.append(_ESCAPES[ch])
|
|
728
|
-
elif code < 0x20:
|
|
729
|
-
out.append("\\u%04x" % code)
|
|
730
|
-
else:
|
|
731
|
-
out.append(ch)
|
|
732
|
-
out.append('"')
|
|
733
|
-
return "".join(out)
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
def jcs(value: Any) -> str:
|
|
737
|
-
"""RFC 8785: members sorted by UTF-16 code units, numbers in ECMAScript form, strings as JSON.stringify
|
|
738
|
-
escapes them. Refuses what has no canonical form: a non-finite number, a lone surrogate, a non-JSON value.
|
|
739
|
-
A JSON number is a double: an integer beyond 2**53 is written as JavaScript would write it."""
|
|
740
|
-
if value is None:
|
|
741
|
-
return "null"
|
|
742
|
-
if value is True:
|
|
743
|
-
return "true"
|
|
744
|
-
if value is False:
|
|
745
|
-
return "false"
|
|
746
|
-
if isinstance(value, int):
|
|
747
|
-
if abs(value) < 2 ** 53:
|
|
748
|
-
return str(value)
|
|
749
|
-
try:
|
|
750
|
-
return _number(float(value))
|
|
751
|
-
except OverflowError:
|
|
752
|
-
raise TypeError("a number beyond a double has no canonical form") from None
|
|
753
|
-
if isinstance(value, float):
|
|
754
|
-
if not math.isfinite(value):
|
|
755
|
-
raise TypeError("a non-finite number has no canonical form")
|
|
756
|
-
return _number(value)
|
|
757
|
-
if isinstance(value, str):
|
|
758
|
-
return _string(value)
|
|
759
|
-
if isinstance(value, list):
|
|
760
|
-
return "[" + ",".join(jcs(item) for item in value) + "]"
|
|
761
|
-
if isinstance(value, dict):
|
|
762
|
-
if not all(isinstance(key, str) for key in value):
|
|
763
|
-
raise TypeError("only string member names have a canonical form")
|
|
764
|
-
keys = sorted(value, key=lambda key: key.encode("utf-16-be", "surrogatepass"))
|
|
765
|
-
return "{" + ",".join(_string(key) + ":" + jcs(value[key]) for key in keys) + "}"
|
|
766
|
-
raise TypeError("%s has no canonical form" % type(value).__name__)
|
|
767
|
-
|
|
768
608
|
|
|
769
609
|
def action_digest(action: Any) -> str:
|
|
770
610
|
"""The digest an approval binds: sha256: and the hex SHA-256 of the action's RFC 8785 form."""
|
|
@@ -789,18 +629,60 @@ class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
|
789
629
|
return None
|
|
790
630
|
|
|
791
631
|
|
|
792
|
-
|
|
632
|
+
def _opener(deadline: Dict[str, Any]) -> urllib.request.OpenerDirector:
|
|
633
|
+
"""No redirect, and a timer that shuts the socket down at the deadline: a socket timeout bounds one read, so an
|
|
634
|
+
issuer trickling a byte a second would otherwise hold the caller for as long as it likes (finding 13)."""
|
|
635
|
+
def watched(base: type) -> type:
|
|
636
|
+
class Watched(base): # type: ignore[misc, valid-type]
|
|
637
|
+
def connect(self) -> None:
|
|
638
|
+
super().connect()
|
|
639
|
+
sock = self.sock
|
|
640
|
+
|
|
641
|
+
def fire() -> None:
|
|
642
|
+
deadline["fired"] = True
|
|
643
|
+
try:
|
|
644
|
+
sock.shutdown(2) # socket.SHUT_RDWR
|
|
645
|
+
except OSError:
|
|
646
|
+
pass
|
|
647
|
+
timer = threading.Timer(max(0.0, deadline["end"] - time.monotonic()), fire)
|
|
648
|
+
timer.daemon = True
|
|
649
|
+
deadline["timer"] = timer
|
|
650
|
+
timer.start()
|
|
651
|
+
return Watched
|
|
652
|
+
|
|
653
|
+
class Http(urllib.request.HTTPHandler):
|
|
654
|
+
def http_open(self, req: Any) -> Any:
|
|
655
|
+
return self.do_open(watched(http.client.HTTPConnection), req)
|
|
656
|
+
|
|
657
|
+
class Https(urllib.request.HTTPSHandler):
|
|
658
|
+
def https_open(self, req: Any) -> Any:
|
|
659
|
+
return self.do_open(watched(http.client.HTTPSConnection), req, context=self._context)
|
|
660
|
+
return urllib.request.build_opener(_NoRedirect, Http, Https)
|
|
793
661
|
|
|
794
662
|
|
|
795
663
|
def _fetch(source: str) -> Any:
|
|
796
664
|
if source.startswith(("https://", "http://")):
|
|
797
665
|
request = urllib.request.Request(source, headers={"Accept": "application/json"})
|
|
666
|
+
deadline: Dict[str, Any] = {"end": time.monotonic() + JWKS_FETCH_SECONDS, "fired": False, "timer": None}
|
|
798
667
|
try:
|
|
799
|
-
with
|
|
800
|
-
|
|
668
|
+
with _opener(deadline).open(request, timeout=JWKS_FETCH_SECONDS) as response: # https or loopback http only
|
|
669
|
+
body = b""
|
|
670
|
+
while len(body) <= 65536 and not deadline["fired"]:
|
|
671
|
+
chunk = response.read1(65537 - len(body))
|
|
672
|
+
if not chunk:
|
|
673
|
+
break
|
|
674
|
+
body += chunk
|
|
675
|
+
if deadline["fired"] or time.monotonic() > deadline["end"]:
|
|
676
|
+
raise TimeoutError("the key set did not arrive within %s s" % JWKS_FETCH_SECONDS)
|
|
677
|
+
return json.loads(body[:65536])
|
|
801
678
|
except urllib.error.HTTPError as exc:
|
|
802
679
|
exc.close()
|
|
803
680
|
raise
|
|
681
|
+
except http.client.HTTPException as exc: # a garbage status line or headers: the issuer cannot be read
|
|
682
|
+
raise ConnectionError(type(exc).__name__) from None
|
|
683
|
+
finally:
|
|
684
|
+
if deadline["timer"] is not None:
|
|
685
|
+
deadline["timer"].cancel()
|
|
804
686
|
return json.loads(Path(source).expanduser().read_text())
|
|
805
687
|
|
|
806
688
|
|
|
@@ -828,28 +710,37 @@ class JwksCache:
|
|
|
828
710
|
self._tried_at = -math.inf
|
|
829
711
|
self._reread_at = -math.inf # the last re-read for a kid the set did not hold
|
|
830
712
|
self._lock = threading.Lock()
|
|
831
|
-
|
|
832
|
-
def _read(self, now: float) -> None:
|
|
833
|
-
self._tried_at = now
|
|
834
|
-
try:
|
|
835
|
-
value = self._fetch(self.source)
|
|
836
|
-
except (OSError, ValueError):
|
|
837
|
-
return
|
|
838
|
-
if _has_keys(value):
|
|
839
|
-
self._keys, self._read_at = value, now
|
|
713
|
+
self._pending: Optional[threading.Event] = None
|
|
840
714
|
|
|
841
715
|
def get(self, kid: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
|
716
|
+
"""The key set, or None. The read runs outside the lock (finding 13): a caller whose kid the set holds never
|
|
717
|
+
waits on another caller's fetch, and one read at a time goes out, as Node's `pending` does."""
|
|
718
|
+
leader, waiting = False, None
|
|
842
719
|
with self._lock:
|
|
843
720
|
now = self._clock()
|
|
844
721
|
fresh = now - self._read_at < JWKS_MAX_AGE_SECONDS
|
|
845
722
|
known = self._keys is not None and (kid is None or any(isinstance(k, dict) and k.get("kid") == kid for k in self._keys["keys"]))
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
self._read(now)
|
|
849
|
-
elif not known and now - self._reread_at >= JWKS_REREAD_SECONDS:
|
|
723
|
+
due = (not fresh and now - self._tried_at >= JWKS_RETRY_SECONDS) or (fresh and not known and now - self._reread_at >= JWKS_REREAD_SECONDS)
|
|
724
|
+
if due and fresh:
|
|
850
725
|
self._reread_at = now
|
|
851
|
-
|
|
852
|
-
|
|
726
|
+
if due and self._pending is None:
|
|
727
|
+
self._pending, self._tried_at, leader = threading.Event(), now, True
|
|
728
|
+
elif due:
|
|
729
|
+
waiting = self._pending
|
|
730
|
+
if leader:
|
|
731
|
+
try:
|
|
732
|
+
value = self._fetch(self.source)
|
|
733
|
+
except Exception: # noqa: BLE001 - whatever a source raises leaves the set unread; the age check decides
|
|
734
|
+
value = None
|
|
735
|
+
with self._lock:
|
|
736
|
+
if _has_keys(value):
|
|
737
|
+
self._keys, self._read_at = value, now
|
|
738
|
+
done, self._pending = self._pending, None
|
|
739
|
+
done.set()
|
|
740
|
+
elif waiting is not None:
|
|
741
|
+
waiting.wait(JWKS_FETCH_SECONDS + 1)
|
|
742
|
+
with self._lock:
|
|
743
|
+
return self._keys if self._clock() - self._read_at < JWKS_MAX_AGE_SECONDS else None
|
|
853
744
|
|
|
854
745
|
|
|
855
746
|
def _kid_of(token: str) -> Optional[str]:
|
|
@@ -864,8 +755,8 @@ TIERS = ("T0", "T1", "T2")
|
|
|
864
755
|
|
|
865
756
|
|
|
866
757
|
def delegation_gate(*, audience: str, jwks: Any, effects: Mapping[str, str], issuer: str,
|
|
867
|
-
read_only: bool = False, clock: Callable[[], float] = time.time, estate: Any =
|
|
868
|
-
estate_applied: Any =
|
|
758
|
+
read_only: bool = False, clock: Callable[[], float] = time.time, estate: Any = _MISSING,
|
|
759
|
+
estate_applied: Any = _MISSING, estate_revocations: Any = _MISSING) -> Callable[..., Dict[str, Any]]:
|
|
869
760
|
"""A check for one tools/call: `gate(tool, headers, tier="T2")` answers the accepted delegation or a DL-19
|
|
870
761
|
refusal. `effects` maps each tool to its declared effect class (its manifest entry); a tool with none is
|
|
871
762
|
refused rather than guessed. `jwks` is a JwksCache or a key set; `issuer` (auth.delegation.issuer) is required.
|