@passioncode-ai/fabric-agent-adapter 0.39.1 → 0.40.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +9 -3
  3. package/package.json +1 -1
  4. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  5. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
  6. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +15 -4
  7. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +64 -0
  8. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +15 -0
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/knowledge.example.json +13 -0
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +264 -0
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +93 -0
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +3 -2
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +128 -0
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +2 -0
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +10 -183
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +204 -0
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +286 -0
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +327 -0
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +47 -0
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +79 -0
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +840 -0
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +340 -0
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +42 -1237
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +1 -16
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +41 -0
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +13 -45
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +387 -0
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +8 -67
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +52 -0
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +231 -0
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -2
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +67 -0
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +202 -0
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +136 -0
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +335 -0
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +2 -12
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +69 -0
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +164 -0
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -32
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +122 -0
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +177 -2541
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -8
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +400 -0
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +46 -0
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +458 -0
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +402 -0
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +399 -0
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +78 -0
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +64 -1398
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +116 -0
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +902 -0
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +457 -0
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +36 -38
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +289 -0
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +15 -180
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +586 -0
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +39 -400
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +137 -0
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +402 -0
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +1 -6
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +310 -0
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_kit.py +243 -0
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +308 -0
  64. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +202 -0
  65. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +431 -0
  66. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +21 -43
  67. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +102 -0
  68. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +338 -0
  69. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +31 -38
  70. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +173 -0
  71. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +298 -3450
  72. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +8 -8
  73. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +412 -0
  74. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +120 -0
  75. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +697 -0
  76. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +1526 -0
  77. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
  78. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +3 -0
  79. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
  80. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +116 -1392
  81. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +270 -0
  82. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +508 -0
  83. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py +265 -0
  84. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +155 -0
  85. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +335 -0
  86. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +1 -0
@@ -62,8 +62,8 @@ def _finite(value: float) -> bool:
62
62
  def _register_ok(kind: str, fields: Dict[str, Any]) -> bool:
63
63
  if kind == ENROLL_TOKEN_TYPE:
64
64
  return (_matches(_ESTATE_SCOPE, fields["scope"]) and _matches(_SHA256, fields["genesis"]) and fields["purpose"] == ENROLL_PURPOSE
65
- and (fields["ceiling"] is None or _matches(_SHA256, fields["ceiling"])) and _finite(_estate.estate_time(fields["notBefore"]))
66
- and _finite(_estate.estate_time(fields["expiresAt"])) and _matches(_NONCE32, fields["nonce"]) and _is_one(fields["uses"]))
65
+ and (fields["ceiling"] is None or _matches(_SHA256, fields["ceiling"])) and _finite(_ftime.parse_rfc3339_ms(fields["notBefore"]))
66
+ and _finite(_ftime.parse_rfc3339_ms(fields["expiresAt"])) and _matches(_NONCE32, fields["nonce"]) and _is_one(fields["uses"]))
67
67
  return _matches(_SHA256, fields["digest"]) and _matches(_NONCE32, fields["nonce"])
68
68
 
69
69
 
@@ -77,7 +77,7 @@ def signing_statement(kind: str, fields: Any) -> Optional[bytes]:
77
77
  if not _register_ok(kind, fields):
78
78
  return None
79
79
  try:
80
- return _delegation.jcs(dict(fields, type=kind)).encode("utf-8")
80
+ return _jsv.jcs(dict(fields, type=kind)).encode("utf-8")
81
81
  except (TypeError, ValueError):
82
82
  return None
83
83
 
@@ -100,7 +100,7 @@ def verify_enroll_token(token: Any, *, record: Any, genesis: Any, at: Any) -> Di
100
100
  clock (an aware datetime or epoch seconds). The first failure answers: no-clock, malformed, wrong-estate, the passkey
101
101
  (no-trusted-authority, revoked-authority, duplicate-authority, unknown-authority), the assertion, then the window
102
102
  (too-long-lived, not-yet-valid, expired). Success: ``{"ok": True, "nonce", "expiresAt", "signedBy"}``."""
103
- now = _estate._clock(at)
103
+ now = _ftime.clock_ms(at)
104
104
  if not _finite(now):
105
105
  return {"ok": False, "problem": "no-clock"}
106
106
  fields = _statement_fields(token)
@@ -112,31 +112,31 @@ def verify_enroll_token(token: Any, *, record: Any, genesis: Any, at: Any) -> Di
112
112
  return {"ok": False, "problem": "malformed"}
113
113
  if fields["scope"] != "estate:%s" % record["estate"] or not isinstance(genesis, str) or fields["genesis"] != genesis:
114
114
  return {"ok": False, "problem": "wrong-estate"}
115
- authorities = _estate._list(record.get("authorities"))
115
+ authorities = _jsv.objects(record.get("authorities"))
116
116
  if not authorities:
117
117
  return {"ok": False, "problem": "no-trusted-authority"}
118
118
  credential_id = signature["credentialId"]
119
- if any(entry.get("kind") == "authority" and _estate._same(entry.get("ref", _estate._MISSING), credential_id) for entry in _estate._list(record.get("revocations"))):
119
+ if any(entry.get("kind") == "authority" and _jsv.same(entry.get("ref", _jsv.MISSING), credential_id) for entry in _jsv.objects(record.get("revocations"))):
120
120
  return {"ok": False, "problem": "revoked-authority"}
121
- matches = [entry for entry in authorities if _estate._same(entry.get("credentialId", _estate._MISSING), credential_id)]
121
+ matches = [entry for entry in authorities if _jsv.same(entry.get("credentialId", _jsv.MISSING), credential_id)]
122
122
  if len(matches) > 1:
123
123
  return {"ok": False, "problem": "duplicate-authority"}
124
124
  if not matches:
125
125
  return {"ok": False, "problem": "unknown-authority"}
126
126
  authority = matches[0]
127
127
  origins = authority.get("origins")
128
- rp_id = _estate._show(authority.get("rpId", _estate._MISSING))
128
+ rp_id = _jsv.show(authority.get("rpId", _jsv.MISSING))
129
129
  if not _estate.is_signing_rp_id(rp_id):
130
130
  return {"ok": False, "problem": "wrong-rp"}
131
- credential = {"credentialId": credential_id, "alg": _estate._show(authority.get("alg", _estate._MISSING)),
131
+ credential = {"credentialId": credential_id, "alg": _jsv.show(authority.get("alg", _jsv.MISSING)),
132
132
  "publicKey": authority["publicKey"] if isinstance(authority.get("publicKey"), dict) else {}, "rpId": rp_id,
133
133
  "origins": [o for o in origins if isinstance(o, str) and _estate.origin_within(o, rp_id)] if isinstance(origins, list) else []}
134
134
  challenge = hashlib.sha256(signing_statement(ENROLL_TOKEN_TYPE, fields)).digest()
135
135
  assertion = _estate.verify_assertion(signature, credential, challenge)
136
136
  if not assertion["ok"]:
137
137
  return {"ok": False, "problem": assertion["problem"]}
138
- not_before = _estate.estate_time(fields["notBefore"])
139
- expires_at = _estate.estate_time(fields["expiresAt"])
138
+ not_before = _ftime.parse_rfc3339_ms(fields["notBefore"])
139
+ expires_at = _ftime.parse_rfc3339_ms(fields["expiresAt"])
140
140
  if expires_at <= not_before or expires_at - not_before > MAX_ENROLL_TOKEN_SECONDS * 1000:
141
141
  return {"ok": False, "problem": "too-long-lived"}
142
142
  if now < not_before - _estate.CLOCK_SKEW_SECONDS * 1000:
@@ -177,8 +177,8 @@ def _token_findings(value: Dict[str, Any]) -> List[Dict[str, str]]:
177
177
  flag("/statement/purpose", "an enroll token's purpose is agent-host")
178
178
  if not _is_one(statement.get("uses")):
179
179
  flag("/statement/uses", "an enroll token is used once: uses is 1, and the hub binds its nonce on the first use")
180
- not_before = _estate.estate_time(statement.get("notBefore"))
181
- expires_at = _estate.estate_time(statement.get("expiresAt"))
180
+ not_before = _ftime.parse_rfc3339_ms(statement.get("notBefore"))
181
+ expires_at = _ftime.parse_rfc3339_ms(statement.get("expiresAt"))
182
182
  rest = {name: v for name, v in statement.items() if name != "type"}
183
183
  named = not extra and not missing and statement.get("purpose") == ENROLL_PURPOSE and _is_one(statement.get("uses")) and _finite(not_before) and _finite(expires_at)
184
184
  if named and signing_statement(ENROLL_TOKEN_TYPE, rest) is None:
@@ -201,20 +201,21 @@ _PROOF = re.compile(r"[A-Za-z0-9_-]{85}[AQgw]\Z")
201
201
 
202
202
 
203
203
  def _b64url(text: str) -> bytes:
204
+ """Strict base64url decode: a length the alphabet cannot produce raises, as the ceremony's readers expect."""
204
205
  return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))
205
206
 
206
207
 
207
208
  def _es256_raw(octets: bytes, message: bytes, raw: bytes) -> bool:
208
209
  """ECDSA P-256 over SHA-256 with a 64-byte r || s signature, low-S only (relay-auth/1's form)."""
209
- if len(octets) != 65 or octets[0] != 4 or not _estate.is_valid_p256_key(octets) or len(raw) != 64:
210
+ if len(octets) != 65 or octets[0] != 4 or not _crypto.is_valid_p256_key(octets) or len(raw) != 64:
210
211
  return False
211
212
  r, s = int.from_bytes(raw[:32], "big"), int.from_bytes(raw[32:], "big")
212
- n = _estate._N
213
+ n = _crypto._P256_N
213
214
  if not (1 <= r < n and 1 <= s <= n // 2):
214
215
  return False
215
216
  e = int.from_bytes(hashlib.sha256(message).digest(), "big")
216
217
  w = pow(s, -1, n)
217
- point = _estate._shamir(e * w % n, r * w % n, (int.from_bytes(octets[1:33], "big"), int.from_bytes(octets[33:], "big")))
218
+ point = _crypto._shamir(e * w % n, r * w % n, (int.from_bytes(octets[1:33], "big"), int.from_bytes(octets[33:], "big")))
218
219
  return point is not None and point[0] % n == r
219
220
 
220
221
 
@@ -227,7 +228,7 @@ def _proves(key: Dict[str, Any], message: str, proof: Any) -> bool:
227
228
  signature = _b64url(proof)
228
229
  if key.get("alg") == "es256":
229
230
  return _es256_raw(octets, message.encode("utf-8"), signature)
230
- return _delegation.ed25519_verify(octets, message.encode("utf-8"), signature)
231
+ return _crypto.ed25519_verify(octets, message.encode("utf-8"), signature)
231
232
 
232
233
 
233
234
  def redeem_enroll_token(token: Any, machine_key: Any, proof: Any, ledger: Any) -> Dict[str, Any]:
@@ -244,11 +245,11 @@ def redeem_enroll_token(token: Any, machine_key: Any, proof: Any, ledger: Any) -
244
245
  if not _proves(machine_key, join, proof):
245
246
  return {"ok": False, "problem": "bad-proof"}
246
247
  fingerprint = _estate.device_key_fingerprint(machine_key)
247
- entry = ledger.get(nonce, _estate._MISSING)
248
+ entry = ledger.get(nonce, _jsv.MISSING)
248
249
  if isinstance(entry, dict) and entry.get("state") == "revoked":
249
250
  return {"ok": False, "problem": "revoked"}
250
- if entry is not _estate._MISSING:
251
- bound = _estate._show(entry.get("fingerprint", _estate._MISSING)) if isinstance(entry, dict) else _estate._show(entry)
251
+ if entry is not _jsv.MISSING:
252
+ bound = _jsv.show(entry.get("fingerprint", _jsv.MISSING)) if isinstance(entry, dict) else _jsv.show(entry)
252
253
  return {"ok": False, "problem": "reused", "log": {"nonce": nonce, "presented": fingerprint, "boundTo": bound}}
253
254
  return {"ok": True, "record": {"nonce": nonce, "state": "redeemed", "fingerprint": fingerprint}}
254
255
 
@@ -268,13 +269,10 @@ _KEY_MEMBERS = ["alg", "kid", "publicKey", "use"]
268
269
  _ALGORITHMS = {"sig": ("es256", "ed25519"), "kem": ("ecdh-p256",)}
269
270
 
270
271
 
271
- def _json_equal(a: Any, b: Any) -> bool:
272
- """The contract's jsonEqual: arrays in order, objects by their members, primitives as JavaScript's ===."""
273
- if isinstance(a, list) or isinstance(b, list):
274
- return isinstance(a, list) and isinstance(b, list) and len(a) == len(b) and all(_json_equal(x, y) for x, y in zip(a, b))
275
- if isinstance(a, dict) or isinstance(b, dict):
276
- return isinstance(a, dict) and isinstance(b, dict) and len(a) == len(b) and all(k in b and _json_equal(a[k], b[k]) for k in a)
277
- return _estate._same(a, b)
272
+ from fabric_jsvalue import json_equal as _json_equal # noqa: E402,F401 - moved (FAA-65)
273
+ import fabric_time as _ftime # noqa: E402 - the core (FAA-65)
274
+ import fabric_jsvalue as _jsv # noqa: E402 - the core (FAA-65)
275
+ import fabric_crypto as _crypto # noqa: E402 - the core (FAA-65)
278
276
 
279
277
 
280
278
  def _is_role(value: Any) -> bool:
@@ -337,7 +335,7 @@ def pairing_commit(side: Any) -> Dict[str, Any]:
337
335
  problem = _side_problem(side)
338
336
  if problem:
339
337
  return {"ok": False, "problem": problem}
340
- return {"ok": True, "commit": "sha256:" + _sha_hex(_delegation.jcs(dict({"type": COMMIT_TYPE, "role": side["role"]}, **_covered(side))))}
338
+ return {"ok": True, "commit": "sha256:" + _sha_hex(_jsv.jcs(dict({"type": COMMIT_TYPE, "role": side["role"]}, **_covered(side))))}
341
339
 
342
340
 
343
341
  def _message(value: Any, step: str) -> Optional[Dict[str, Any]]:
@@ -354,7 +352,7 @@ def check_reveal(commit: Any, reveal: Any, expected: Optional[str] = None) -> Di
354
352
  c, r = _message(commit, "commit"), _message(reveal, "reveal")
355
353
  if c is None or r is None:
356
354
  return {"ok": False, "problem": "malformed"}
357
- if not _estate._same(r["role"], c["role"]) or (expected is not None and not _estate._same(r["role"], expected)):
355
+ if not _jsv.same(r["role"], c["role"]) or (expected is not None and not _jsv.same(r["role"], expected)):
358
356
  return {"ok": False, "problem": "role-mismatch"}
359
357
  made = pairing_commit({k: v for k, v in r.items() if k not in ("schema", "step")})
360
358
  if not made["ok"]:
@@ -369,14 +367,14 @@ def comparison_code(new_reveal: Any, trusted_reveal: Any) -> Dict[str, Any]:
369
367
  a, b = _message(new_reveal, "reveal"), _message(trusted_reveal, "reveal")
370
368
  if a is None or b is None:
371
369
  return {"ok": False, "problem": "malformed"}
372
- if not _estate._same(a["role"], "new") or not _estate._same(b["role"], "trusted"):
370
+ if not _jsv.same(a["role"], "new") or not _jsv.same(b["role"], "trusted"):
373
371
  return {"ok": False, "problem": "role-mismatch" if _is_role(a["role"]) and _is_role(b["role"]) else "bad-role"}
374
372
  problem = _side_problem(a) or _side_problem(b)
375
373
  if problem:
376
374
  return {"ok": False, "problem": problem}
377
375
  if machine_of_keys(a["keys"]) == machine_of_keys(b["keys"]):
378
376
  return {"ok": False, "problem": "same-machine"}
379
- digest = hashlib.sha256(_delegation.jcs({"type": CODE_TYPE, "new": _covered(a), "trusted": _covered(b)}).encode("utf-8")).digest()
377
+ digest = hashlib.sha256(_jsv.jcs({"type": CODE_TYPE, "new": _covered(a), "trusted": _covered(b)}).encode("utf-8")).digest()
380
378
  return {"ok": True, "code": str(int.from_bytes(digest[:5], "big") % 10 ** CODE_DIGITS).zfill(CODE_DIGITS)}
381
379
 
382
380
 
@@ -384,7 +382,7 @@ def estate_fingerprint(digest: Any) -> Dict[str, Any]:
384
382
  """The estate's fingerprint from revision 1's digest: five groups of five digits a person compares."""
385
383
  if not _matches(_SHA256, digest):
386
384
  return {"ok": False, "problem": "malformed"}
387
- f = hashlib.sha256(_delegation.jcs({"type": FINGERPRINT_TYPE, "digest": digest}).encode("utf-8")).digest()
385
+ f = hashlib.sha256(_jsv.jcs({"type": FINGERPRINT_TYPE, "digest": digest}).encode("utf-8")).digest()
388
386
  groups = [str(int.from_bytes(f[5 * i:5 * i + 5], "big") % 10 ** FINGERPRINT_GROUP_DIGITS).zfill(FINGERPRINT_GROUP_DIGITS) for i in range(FINGERPRINT_GROUPS)]
389
387
  return {"ok": True, "fingerprint": " ".join(groups)}
390
388
 
@@ -403,8 +401,8 @@ def checked_comparison_code(new_commit: Any, new_reveal: Any, trusted_commit: An
403
401
  def rendezvous_verdict(opened_at: Any, messages: Any, at: Any) -> Dict[str, Any]:
404
402
  """A one-time rendezvous after the messages it carried, at ``at`` (an aware datetime or epoch seconds): open,
405
403
  complete, or burned with the reason and the index of the message that burned it."""
406
- opened = _estate.estate_time(opened_at)
407
- now = _estate._clock(at)
404
+ opened = _ftime.parse_rfc3339_ms(opened_at)
405
+ now = _ftime.clock_ms(at)
408
406
  if not _finite(opened) or not _finite(now) or not isinstance(messages, list):
409
407
  return {"state": "burned", "reason": "malformed", "index": -1}
410
408
  if now > opened + RENDEZVOUS_SECONDS * 1000 or now < opened - _estate.CLOCK_SKEW_SECONDS * 1000:
@@ -443,21 +441,21 @@ def _machine_of(record: Dict[str, Any], machine_id: Any) -> Optional[Dict[str, A
443
441
  machines = record.get("machines")
444
442
  if not isinstance(machines, list):
445
443
  return None
446
- return next((m for m in machines if isinstance(m, dict) and _estate._same(m.get("id", _estate._MISSING), machine_id)), None)
444
+ return next((m for m in machines if isinstance(m, dict) and _jsv.same(m.get("id", _jsv.MISSING), machine_id)), None)
447
445
 
448
446
 
449
447
  def _machine_revoked(record: Dict[str, Any], machine_id: Any) -> bool:
450
448
  revocations = record.get("revocations")
451
449
  return isinstance(revocations, list) and any(isinstance(e, dict) and e.get("kind") == "machine"
452
- and _estate._same(e.get("ref", _estate._MISSING), machine_id) for e in revocations)
450
+ and _jsv.same(e.get("ref", _jsv.MISSING), machine_id) for e in revocations)
453
451
 
454
452
 
455
453
  def join_listed(record: Any, new_reveal: Any, trusted_reveal: Any, genesis: Any) -> Dict[str, Any]:
456
454
  """What the new machine checks before it pins and applies: the estate is the one the code bound, and the record
457
455
  lists it with exactly the keys it revealed and the trusted machine with its revealed #sig key, revoking neither."""
458
456
  a, b = _message(new_reveal, "reveal"), _message(trusted_reveal, "reveal")
459
- if (not isinstance(record, dict) or a is None or b is None or not _estate._same(a["role"], "new")
460
- or not _estate._same(b["role"], "trusted") or _side_problem(a) or _side_problem(b)):
457
+ if (not isinstance(record, dict) or a is None or b is None or not _jsv.same(a["role"], "new")
458
+ or not _jsv.same(b["role"], "trusted") or _side_problem(a) or _side_problem(b)):
461
459
  return {"ok": False, "problem": "malformed"}
462
460
  if not isinstance(genesis, str) or b["genesis"] != genesis:
463
461
  return {"ok": False, "problem": "other-estate"}
@@ -0,0 +1,289 @@
1
+ """The kit's cryptography, once (FAA-65, layer L1): what the contract's verifiers need and nothing more.
2
+
3
+ - ``ed25519_verify(public_key, message, signature)`` (RFC 8032) and ``is_small_order_key`` with
4
+ ``SMALL_ORDER_ENCODINGS``: a key of small order verifies forged signatures and is refused (DEC-0045).
5
+ - ``is_valid_p256_key`` and ``es256_verify`` (ECDSA P-256 over SHA-256, DER or raw signatures) for passkeys.
6
+ - base64url without padding (``b64url``, ``b64url_bytes``, ``node_base64``) and ``sha256``.
7
+
8
+ Pure Python over integers: slow next to OpenSSL, constant inputs only, verification only. Nothing here signs.
9
+ Standard library only; imports only ``fabric_jsvalue``.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import base64
14
+ import hashlib
15
+ import re
16
+ from typing import Any, Optional, Tuple
17
+ import secrets
18
+
19
+
20
+ # The small-order points of edwards25519 and their non-canonical encodings, sign bit cleared (the contract's
21
+ # src/ed25519.ts; libsodium's has_small_order list).
22
+ SMALL_ORDER_ENCODINGS = (
23
+ "0000000000000000000000000000000000000000000000000000000000000000", "0100000000000000000000000000000000000000000000000000000000000000",
24
+ "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05", "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
25
+ "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
26
+ "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
27
+ )
28
+
29
+
30
+ def is_small_order_key(raw: bytes) -> bool:
31
+ """Whether a 32-byte Ed25519 public key is a point of small order, in any encoding (CT-29)."""
32
+ if len(raw) != 32:
33
+ return True
34
+ return (raw[:31] + bytes([raw[31] & 0x7F])).hex() in SMALL_ORDER_ENCODINGS
35
+
36
+
37
+ _ED_P = 2 ** 255 - 19
38
+
39
+
40
+ _ED_L = 2 ** 252 + 27742317777372353535851937790883648493
41
+
42
+
43
+ _ED_D = (-121665 * pow(121666, _ED_P - 2, _ED_P)) % _ED_P
44
+
45
+
46
+ _ED_SQRT_M1 = pow(2, (_ED_P - 1) // 4, _ED_P)
47
+
48
+
49
+ def _ed_add(a: Tuple[int, int, int, int], b: Tuple[int, int, int, int]) -> Tuple[int, int, int, int]:
50
+ x1, y1, z1, t1 = a
51
+ x2, y2, z2, t2 = b
52
+ aa = (y1 - x1) * (y2 - x2) % _ED_P
53
+ bb = (y1 + x1) * (y2 + x2) % _ED_P
54
+ cc = 2 * t1 * t2 * _ED_D % _ED_P
55
+ dd = 2 * z1 * z2 % _ED_P
56
+ e, f, g, h = bb - aa, dd - cc, dd + cc, bb + aa
57
+ return (e * f % _ED_P, g * h % _ED_P, f * g % _ED_P, e * h % _ED_P)
58
+
59
+
60
+ def _ed_mul(scalar: int, point: Tuple[int, int, int, int]) -> Tuple[int, int, int, int]:
61
+ result = (0, 1, 1, 0)
62
+ while scalar:
63
+ if scalar & 1:
64
+ result = _ed_add(result, point)
65
+ point = _ed_add(point, point)
66
+ scalar >>= 1
67
+ return result
68
+
69
+
70
+ def _ed_decode_point(data: bytes) -> Optional[Tuple[int, int, int, int]]:
71
+ if len(data) != 32:
72
+ return None
73
+ y = int.from_bytes(data, "little")
74
+ sign = y >> 255
75
+ y &= (1 << 255) - 1
76
+ if y >= _ED_P:
77
+ return None
78
+ x2 = (y * y - 1) * pow(_ED_D * y * y + 1, _ED_P - 2, _ED_P) % _ED_P
79
+ if x2 == 0:
80
+ if sign:
81
+ return None
82
+ return (0, y, 1, 0)
83
+ x = pow(x2, (_ED_P + 3) // 8, _ED_P)
84
+ if (x * x - x2) % _ED_P:
85
+ x = x * _ED_SQRT_M1 % _ED_P
86
+ if (x * x - x2) % _ED_P:
87
+ return None
88
+ if x & 1 != sign:
89
+ x = _ED_P - x
90
+ return (x, y, 1, x * y % _ED_P)
91
+
92
+
93
+ def _ed_encode_point(point: Tuple[int, int, int, int]) -> bytes:
94
+ x, y, z, _ = point
95
+ zi = pow(z, _ED_P - 2, _ED_P)
96
+ x, y = x * zi % _ED_P, y * zi % _ED_P
97
+ return (y | ((x & 1) << 255)).to_bytes(32, "little")
98
+
99
+
100
+ _ED_G = _ed_decode_point(bytes.fromhex("5866666666666666666666666666666666666666666666666666666666666666"))
101
+
102
+
103
+ _ED_IDENTITY = (1).to_bytes(32, "little")
104
+
105
+
106
+ def ed25519_verify(public_key: bytes, message: bytes, signature: bytes) -> bool:
107
+ """RFC 8032 §5.1.7, cofactorless: [S]B == R + [k]A, compared by R's encoding. False, never an exception."""
108
+ if len(public_key) != 32 or len(signature) != 64:
109
+ return False
110
+ s = int.from_bytes(signature[32:], "little")
111
+ if s >= _ED_L:
112
+ return False
113
+ a = _ed_decode_point(public_key)
114
+ if a is None or _ed_encode_point(_ed_mul(8, a)) == _ED_IDENTITY:
115
+ return False # a small-order key would accept R = [S]B for a quarter or more of all messages; OpenSSL refuses it
116
+ k = int.from_bytes(hashlib.sha512(signature[:32] + public_key + message).digest(), "little") % _ED_L
117
+ neg_a = ((-a[0]) % _ED_P, a[1], a[2], (-a[3]) % _ED_P)
118
+ return secrets.compare_digest(_ed_encode_point(_ed_add(_ed_mul(s, _ED_G), _ed_mul(k, neg_a))), signature[:32])
119
+
120
+
121
+ def b64url_bytes(text: str) -> bytes:
122
+ """Buffer.from(text, "base64url") for text of the base64url alphabet: a dangling sixth bit group is ignored."""
123
+ return b64url_decode(text)
124
+
125
+
126
+ def b64url(data: bytes) -> str:
127
+ return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
128
+
129
+
130
+ def node_base64(text: str) -> bytes:
131
+ """Buffer.from(text, "base64"): either alphabet, other characters skipped, stops at the first "=", a dangling
132
+ sixth bit group ignored."""
133
+ kept = []
134
+ for ch in text:
135
+ if ch == "=":
136
+ break
137
+ if ch.isascii() and (ch.isalnum() or ch in "+/-_"):
138
+ kept.append({"-": "+", "_": "/"}.get(ch, ch))
139
+ body = "".join(kept)
140
+ if len(body) % 4 == 1:
141
+ body = body[:-1]
142
+ return base64.b64decode(body + "=" * (-len(body) % 4))
143
+
144
+
145
+ def sha256(data: bytes) -> bytes:
146
+ return hashlib.sha256(data).digest()
147
+
148
+
149
+ _P256_P = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
150
+
151
+
152
+ _P256_A = _P256_P - 3
153
+
154
+
155
+ _P256_B = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
156
+
157
+
158
+ _P256_N = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
159
+
160
+
161
+ _P256_GX = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
162
+
163
+
164
+ _P256_GY = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5
165
+
166
+
167
+ def is_valid_p256_key(raw: bytes) -> bool:
168
+ """SEC 1 §3.2.2 for an uncompressed point: 0x04 ‖ X ‖ Y, X and Y below p, on y² = x³ − 3x + b. The cofactor is 1."""
169
+ if len(raw) != 65 or raw[0] != 0x04:
170
+ return False
171
+ x = int.from_bytes(raw[1:33], "big")
172
+ y = int.from_bytes(raw[33:], "big")
173
+ if x >= _P256_P or y >= _P256_P:
174
+ return False
175
+ return (y * y - (x * x * x + _P256_A * x + _P256_B)) % _P256_P == 0
176
+
177
+
178
+ def _jacobian_double(p: Tuple[int, int, int]) -> Tuple[int, int, int]:
179
+ x, y, z = p
180
+ if y == 0 or z == 0:
181
+ return (0, 1, 0)
182
+ ysq = y * y % _P256_P
183
+ s = 4 * x * ysq % _P256_P
184
+ zsq = z * z % _P256_P
185
+ m = 3 * (x - zsq) * (x + zsq) % _P256_P # a = −3
186
+ nx = (m * m - 2 * s) % _P256_P
187
+ ny = (m * (s - nx) - 8 * ysq * ysq) % _P256_P
188
+ nz = 2 * y * z % _P256_P
189
+ return (nx, ny, nz)
190
+
191
+
192
+ def _jacobian_add(p: Tuple[int, int, int], q: Tuple[int, int, int]) -> Tuple[int, int, int]:
193
+ if p[2] == 0:
194
+ return q
195
+ if q[2] == 0:
196
+ return p
197
+ z1z1 = p[2] * p[2] % _P256_P
198
+ z2z2 = q[2] * q[2] % _P256_P
199
+ u1 = p[0] * z2z2 % _P256_P
200
+ u2 = q[0] * z1z1 % _P256_P
201
+ s1 = p[1] * q[2] * z2z2 % _P256_P
202
+ s2 = q[1] * p[2] * z1z1 % _P256_P
203
+ if u1 == u2:
204
+ return _jacobian_double(p) if s1 == s2 else (0, 1, 0)
205
+ h = (u2 - u1) % _P256_P
206
+ r = (s2 - s1) % _P256_P
207
+ hh = h * h % _P256_P
208
+ hhh = h * hh % _P256_P
209
+ v = u1 * hh % _P256_P
210
+ nx = (r * r - hhh - 2 * v) % _P256_P
211
+ ny = (r * (v - nx) - s1 * hhh) % _P256_P
212
+ nz = h * p[2] * q[2] % _P256_P
213
+ return (nx, ny, nz)
214
+
215
+
216
+ def _shamir(u1: int, u2: int, q: Tuple[int, int]) -> Optional[Tuple[int, int]]:
217
+ """u1·G + u2·Q in affine form, or None at infinity."""
218
+ g = (_P256_GX, _P256_GY, 1)
219
+ qj = (q[0], q[1], 1)
220
+ both = _jacobian_add(g, qj)
221
+ out = (0, 1, 0)
222
+ for bit in range(max(u1.bit_length(), u2.bit_length()) - 1, -1, -1):
223
+ out = _jacobian_double(out)
224
+ a, b = (u1 >> bit) & 1, (u2 >> bit) & 1
225
+ if a and b:
226
+ out = _jacobian_add(out, both)
227
+ elif a:
228
+ out = _jacobian_add(out, g)
229
+ elif b:
230
+ out = _jacobian_add(out, qj)
231
+ if out[2] == 0:
232
+ return None
233
+ zinv = pow(out[2], -1, _P256_P)
234
+ return (out[0] * zinv * zinv % _P256_P, out[1] * zinv * zinv * zinv % _P256_P)
235
+
236
+
237
+ def _der_integer(data: bytes, at: int) -> Tuple[Optional[int], int]:
238
+ if at + 2 > len(data) or data[at] != 0x02:
239
+ return None, at
240
+ length = data[at + 1]
241
+ start = at + 2
242
+ if length == 0 or length > 33 or start + length > len(data):
243
+ return None, at
244
+ body = data[start:start + length]
245
+ if body[0] & 0x80:
246
+ return None, at # negative
247
+ if length > 1 and body[0] == 0 and not body[1] & 0x80:
248
+ return None, at # not minimal
249
+ return int.from_bytes(body, "big"), start + length
250
+
251
+
252
+ def _der_signature(signature: bytes) -> Optional[Tuple[int, int]]:
253
+ """An ASN.1 DER Ecdsa-Sig-Value (RFC 3279 §2.2.3), strictly: one SEQUENCE of two minimal positive INTEGERs."""
254
+ if len(signature) < 8 or signature[0] != 0x30:
255
+ return None
256
+ if signature[1] & 0x80:
257
+ return None # a P-256 signature fits a short-form length
258
+ if signature[1] != len(signature) - 2:
259
+ return None
260
+ r, at = _der_integer(signature, 2)
261
+ if r is None:
262
+ return None
263
+ s, at = _der_integer(signature, at)
264
+ if s is None or at != len(signature):
265
+ return None
266
+ return r, s
267
+
268
+
269
+ def es256_verify(x: bytes, y: bytes, message: bytes, signature: bytes) -> bool:
270
+ """ECDSA P-256 with SHA-256 over `message`, the signature in DER (FIPS 186-5 §6.4.2). False, never an exception."""
271
+ if not is_valid_p256_key(b"\x04" + x + y):
272
+ return False
273
+ parsed = _der_signature(signature)
274
+ if parsed is None:
275
+ return False
276
+ r, s = parsed
277
+ if not (1 <= r < _P256_N and 1 <= s < _P256_N):
278
+ return False
279
+ e = int.from_bytes(sha256(message), "big")
280
+ w = pow(s, -1, _P256_N)
281
+ point = _shamir(e * w % _P256_N, r * w % _P256_N, (int.from_bytes(x, "big"), int.from_bytes(y, "big")))
282
+ return point is not None and point[0] % _P256_N == r
283
+
284
+
285
+ def b64url_decode(segment: str) -> bytes:
286
+ """Base64url without padding, as Node reads it: a dangling sixth bit group is ignored."""
287
+ if len(segment) % 4 == 1:
288
+ segment = segment[:-1]
289
+ return base64.urlsafe_b64decode(segment + "=" * (-len(segment) % 4))