@passioncode-ai/fabric-agent-adapter 0.19.0 → 0.21.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 (20) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +3 -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 +3 -3
  6. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  7. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +7 -4
  8. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +14 -0
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +90 -0
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/profiles.md +23 -0
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +1 -1
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +26 -4
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +443 -0
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +140 -6
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +8 -2
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +32 -6
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +826 -0
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +217 -8
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +7 -1
  20. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
@@ -432,11 +432,30 @@ def _approval_shape(payload: Dict[str, Any]) -> bool:
432
432
  return payload.get("method") in ("prompt", "operator-channel") # DEC-0045: `device` is removed
433
433
 
434
434
 
435
+ def _estate_problem(estate: Any, jwks: Dict[str, Any], kid: str, now: float) -> Optional[Tuple[str, str]]:
436
+ """DL-14 with DEC-0055: a verifier that reads the estate record trusts a kid only while a current revision certifies
437
+ it for a machine and does not revoke it. `estate` is a revision the caller verified (fabric_estate.
438
+ verify_estate_record) or a callable returning the current one; None skips the rule. (refusal kind, detail) or None."""
439
+ if estate is None:
440
+ return None
441
+ try:
442
+ record = estate() if callable(estate) else estate
443
+ except Exception: # an unreadable record certifies nothing; it is never a pass
444
+ return ("issuer", "estate-unreadable")
445
+ import fabric_estate # beside this file; imported here, since fabric_estate imports this module
446
+ jwk = next((k for k in jwks["keys"] if isinstance(k, dict) and k.get("kid") == kid), {})
447
+ if not fabric_estate.hub_key_certified(record, kid, jwk.get("x"), now):
448
+ return ("token", "kid-not-certified")
449
+ return None
450
+
451
+
435
452
  def verify_delegation(token: Any, *, audience: str, jwks: Any, now: float, capability: str, effect: str,
436
- issuer: Optional[str], read_only: bool = False) -> Dict[str, Any]:
453
+ issuer: Optional[str], read_only: bool = False, estate: Any = None) -> Dict[str, Any]:
437
454
  """DL-13, in its fixed order: nothing about a token is believed before its signature is.
438
455
  `now` is seconds since the epoch. Returns {ok: True, claims, principal, caller, chain, kid} or a refusal
439
- {ok: False, code, detail[, data]} whose detail is a short machine word, never the token. `issuer` is required."""
456
+ {ok: False, code, detail[, data]} whose detail is a short machine word, never the token. `issuer` is required.
457
+ With `estate` (a verified estate revision, or a callable returning the current one), the token's kid must be one
458
+ the revision certifies for a machine (DL-14, DEC-0055)."""
440
459
  if not _is_string(issuer):
441
460
  return _refuse("issuer-unavailable", "no-trusted-issuer")
442
461
  if not _has_keys(jwks):
@@ -444,6 +463,9 @@ def verify_delegation(token: Any, *, audience: str, jwks: Any, now: float, capab
444
463
  checked = _authentic(token, jwks, DELEGATION_TYP)
445
464
  if "problem" in checked:
446
465
  return _refuse("issuer-unavailable" if checked.get("issuer") else "unauthenticated", checked["problem"])
466
+ uncertified = _estate_problem(estate, jwks, checked["kid"], now)
467
+ if uncertified:
468
+ return _refuse("issuer-unavailable" if uncertified[0] == "issuer" else "unauthenticated", uncertified[1])
447
469
  claims = checked["compact"]["payload"]
448
470
  problem, depth = _delegation_shape(claims)
449
471
  if problem:
@@ -543,7 +565,7 @@ UNSAFE_NO_JTI_STORE = _NoReplayProtection()
543
565
 
544
566
 
545
567
  def verify_approval(approval: Optional[str], *, audience: str, jwks: Any, now: float, action_digest: str,
546
- jti_store: Any, issuer: Optional[str]) -> Dict[str, Any]:
568
+ jti_store: Any, issuer: Optional[str], estate: Any = None) -> Dict[str, Any]:
547
569
  """DL-18: a missing approval is approval-required carrying the action digest; any fault of the document is
548
570
  approval-invalid; a second use is approval-reused. The jti is consumed last."""
549
571
  if approval is None or approval == "":
@@ -557,6 +579,9 @@ def verify_approval(approval: Optional[str], *, audience: str, jwks: Any, now: f
557
579
  checked = _authentic(approval, jwks, APPROVAL_TYP)
558
580
  if "problem" in checked:
559
581
  return _refuse("issuer-unavailable" if checked.get("issuer") else "approval-invalid", checked["problem"])
582
+ uncertified = _estate_problem(estate, jwks, checked["kid"], now)
583
+ if uncertified:
584
+ return _refuse("issuer-unavailable" if uncertified[0] == "issuer" else "approval-invalid", uncertified[1])
560
585
  claims = checked["compact"]["payload"]
561
586
  if not _approval_shape(claims):
562
587
  return _refuse("approval-invalid", "malformed-claims")
@@ -795,7 +820,7 @@ TIERS = ("T0", "T1", "T2")
795
820
 
796
821
 
797
822
  def delegation_gate(*, audience: str, jwks: Any, effects: Mapping[str, str], issuer: str,
798
- read_only: bool = False, clock: Callable[[], float] = time.time) -> Callable[..., Dict[str, Any]]:
823
+ read_only: bool = False, clock: Callable[[], float] = time.time, estate: Any = None) -> Callable[..., Dict[str, Any]]:
799
824
  """A check for one tools/call: `gate(tool, headers, tier="T2")` answers the accepted delegation or a DL-19
800
825
  refusal. `effects` maps each tool to its declared effect class (its manifest entry); a tool with none is
801
826
  refused rather than guessed. `jwks` is a JwksCache or a key set; `issuer` (auth.delegation.issuer) is required.
@@ -804,7 +829,8 @@ def delegation_gate(*, audience: str, jwks: Any, effects: Mapping[str, str], iss
804
829
  own local floor has authenticated; T1 an enrolled machine of the same owner; T2 everything else, and any call
805
830
  relayed from off the machine. A T0 or T1 caller is never required to present a token: with none, the call
806
831
  passes with no delegation ({ok: True, tier, principal: None, ...}). A present token is verified at every tier
807
- and refused when invalid. Unknown tiers count as T2."""
832
+ and refused when invalid. Unknown tiers count as T2. `estate` (a callable returning the current verified estate
833
+ revision) makes the gate trust only kids that revision certifies (DL-14, DEC-0055)."""
808
834
  if not _is_string(issuer):
809
835
  raise ValueError("the gate needs the issuer it trusts (auth.delegation.issuer)")
810
836
 
@@ -819,7 +845,7 @@ def delegation_gate(*, audience: str, jwks: Any, effects: Mapping[str, str], iss
819
845
  return _refuse("unauthenticated", "no-token")
820
846
  keys = jwks.get(_kid_of(carried["token"])) if isinstance(jwks, JwksCache) else jwks
821
847
  verdict = verify_delegation(carried["token"], audience=audience, jwks=keys, now=clock(), capability=tool,
822
- effect=effect, issuer=issuer, read_only=read_only)
848
+ effect=effect, issuer=issuer, read_only=read_only, estate=estate)
823
849
  if verdict["ok"]:
824
850
  verdict["tier"] = tier if tier in TIERS else "T2"
825
851
  return verdict