@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.
Files changed (123) hide show
  1. package/CHANGELOG.md +203 -0
  2. package/README.md +18 -6
  3. package/SECURITY.md +27 -6
  4. package/bin/fabric-agent-adapter.js +71 -6
  5. package/package.json +2 -2
  6. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +42 -66
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +4 -2
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +55 -0
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +9 -3
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/verification.md +15 -0
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +181 -26
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +66 -5
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +81 -23
  15. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +1 -1
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +42 -33
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +21 -9
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +104 -0
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +15 -0
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/knowledge.example.json +13 -0
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +465 -0
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +21 -5
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +93 -0
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +2 -1
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +4 -2
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +3 -2
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +5 -4
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +5 -0
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +129 -0
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +3 -1
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +5 -1
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +2 -0
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +48 -19
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +66 -5
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +11 -184
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +204 -0
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +53 -19
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +288 -0
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +327 -0
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +47 -0
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +79 -0
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +145 -0
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +848 -0
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +340 -0
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +42 -1237
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +7 -18
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +41 -0
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +15 -46
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +387 -0
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +11 -68
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +52 -0
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +231 -0
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +50 -16
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +67 -0
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +235 -0
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +136 -0
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +335 -0
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +146 -32
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +69 -0
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +164 -0
  64. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -32
  65. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +122 -0
  66. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +177 -2541
  67. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -8
  68. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +404 -0
  69. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +46 -0
  70. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +458 -0
  71. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +408 -0
  72. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +399 -0
  73. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +91 -0
  74. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +64 -1398
  75. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +116 -0
  76. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +162 -0
  77. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +882 -0
  78. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +457 -0
  79. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +36 -39
  80. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +336 -0
  81. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +113 -222
  82. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +586 -0
  83. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +44 -405
  84. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +140 -0
  85. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +402 -0
  86. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +6 -2
  87. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +119 -77
  88. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +310 -0
  89. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_kit.py +243 -0
  90. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +308 -0
  91. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +202 -0
  92. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +431 -0
  93. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +285 -109
  94. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +102 -0
  95. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +338 -0
  96. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +32 -39
  97. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +173 -0
  98. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +1 -1
  99. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +298 -3450
  100. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +8 -8
  101. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +437 -0
  102. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +120 -0
  103. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +697 -0
  104. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +1621 -0
  105. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +25 -7
  106. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +29 -60
  107. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +4 -2
  108. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +3 -0
  109. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +55 -0
  110. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +66 -5
  111. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +3 -3
  112. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/native-host-hooks.md +2 -1
  113. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +26 -13
  114. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +122 -1392
  115. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +278 -0
  116. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +539 -0
  117. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py +324 -0
  118. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +173 -0
  119. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +491 -0
  120. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +11 -1
  121. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/context.mjs +25 -2
  122. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/openclaw/index.mjs +4 -2
  123. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/opencode/fabric.mjs +2 -1
@@ -0,0 +1,116 @@
1
+ """A fixture conversation for the probe (check_service.py, rule board.responder): a scripted board that a responder
2
+ can be run against without Fabric. Part of the board connector (FAA-65, layer L4)."""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+ from typing import Dict
9
+ from typing import List
10
+
11
+ import sys as _sys # noqa: E402
12
+ from pathlib import Path as _Path # noqa: E402
13
+ if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from any working directory (FAA-65)
14
+ _sys.path.insert(0, str(_Path(__file__).resolve().parent))
15
+ import fabric_hub as _hub
16
+ from fabric_board_shapes import ( # noqa: E402 - the kit (FAA-65)
17
+ COMMS_PROTOCOL,
18
+ RESPOND_TOOLS,
19
+ )
20
+ from fabric_board_session import ( # noqa: E402 - the kit (FAA-65)
21
+ BoardSession,
22
+ Responder,
23
+ )
24
+
25
+
26
+ _FIXTURE_PROJECT = "3f2a6b1c-0d4e-4f5a-8b6c-7d8e9f0a1b2c"
27
+
28
+
29
+ _FIXTURE_DIGEST = "sha256:" + "a" * 64
30
+
31
+
32
+ def _fixture_answers(variant: str) -> Dict[str, Any]:
33
+ """Scripted board answers in the contract's receipt shapes (fixtures comms-receipt-enroll, comms-receipt-claim)."""
34
+ lease = "2026-10-13T10:02:00Z"
35
+ status: Dict[str, Any] = {"protocol": COMMS_PROTOCOL, "board": {"state": "ready"}, "project": _FIXTURE_PROJECT, "responders": [],
36
+ "transport": {"telegram": "off"}, "unread": 0, "idempotency": {"epoch": 3}}
37
+ if variant == "no-epoch":
38
+ del status["idempotency"]
39
+ request = {"id": "req_A1b2C3d4E5", "state": "claimed", "effect": "not_started", "revision": 2}
40
+ return {
41
+ "com.status": status,
42
+ "com.enroll": {"ok": True, "repeated": False, "slot": {"project": _FIXTURE_PROJECT, "capability": "fixture.render", "generation": 2, "leaseExpiresAt": lease}},
43
+ "com.claim": {"ok": True, "repeated": False, "message": "msg_K9j8H7g6F5", "request": request,
44
+ "attempt": {"id": "att_Zx9Cv8Bn7M", "generation": 2, "digest": _FIXTURE_DIGEST, "leaseExpiresAt": lease}},
45
+ "com.get": {"id": "msg_K9j8H7g6F5", "thread": "thr_8Hc2kQ9xA1", "kind": "request", "at": "2026-10-13T10:00:00Z", "digest": _FIXTURE_DIGEST,
46
+ "sender": {"project": "proj-requester", "principal": {"kind": "agent", "id": "s-1", "provenance": "trusted"}},
47
+ "body": {"text": "Render the fixture."},
48
+ "request": {"id": "req_A1b2C3d4E5", "target": _FIXTURE_PROJECT, "capability": "fixture.render", "state": "claimed", "effect": "not_started", "revision": 2}},
49
+ "com.accept": ({"error": {"code": "fenced", "message": "A newer responder holds this slot; this call was not committed.", "retryable": False}}
50
+ if variant == "fenced" else {"ok": True, "repeated": False, "request": dict(request, state="accepted", revision=3)}),
51
+ "com.complete": {"ok": True, "repeated": False, "request": dict(request, state="completed", revision=4)},
52
+ }
53
+
54
+
55
+ def self_check() -> List[str]:
56
+ """One scripted conversation per path through this kit, with no Fabric: the whole conversation, a `fenced` accept
57
+ and a board session without an epoch. Answers the problems found; an empty list means this kit answers the fixture
58
+ conversation as the contract asks. Writes only in a temporary folder it removes."""
59
+ import os
60
+ import tempfile
61
+ problems: List[str] = []
62
+ for variant in ("whole", "fenced", "no-epoch"):
63
+ with tempfile.TemporaryDirectory() as tmp:
64
+ root = Path(tmp) / "fabric"
65
+ root.mkdir(mode=0o700)
66
+ door = root / "hub-door-token"
67
+ door.write_text("d" * 43)
68
+ os.chmod(door, 0o600)
69
+ (root / "hub.json").write_text(json.dumps({"protocol": _hub.HUB_PROTOCOL, "origin": "http://127.0.0.1:47070", "mcp": "/mcp",
70
+ "doorTokenFile": str(door), "pid": os.getpid(), "startedAt": "2026-10-13T10:00:00Z"}))
71
+ cred = Path(tmp) / "credential"
72
+ cred.write_text("c" * 43)
73
+ os.chmod(cred, 0o600)
74
+ answers, calls, handled = _fixture_answers(variant), [], []
75
+
76
+ def post(origin, body, bearer, session=None, timeout=15.0, method="POST", protocol_version=None):
77
+ if method == "DELETE":
78
+ return 200, {}, None
79
+ rpc = body.get("method")
80
+ if rpc == "initialize":
81
+ return 200, {"mcp-session-id": "fixture-session"}, {"jsonrpc": "2.0", "id": body.get("id"), "result": {"protocolVersion": _hub.MCP_PROTOCOL_VERSION}}
82
+ if rpc != "tools/call":
83
+ return 202, {}, None
84
+ tool = body["params"]["name"]
85
+ calls.append((tool, body["params"]["arguments"]))
86
+ if tool == "fabric.board.join":
87
+ value = {"session": {"bearer": "b" * 43, "sessionId": "s-fixture", "projectId": _FIXTURE_PROJECT, "capabilities": ["fixture.render"]}}
88
+ return 200, {}, {"jsonrpc": "2.0", "id": body.get("id"), "result": {"structuredContent": value}}
89
+ value = answers.get(tool, {"error": {"code": "unsupported_capability", "message": "not in the fixture", "retryable": False}})
90
+ return 200, {}, {"jsonrpc": "2.0", "id": body.get("id"), "result": {"content": [{"type": "text", "text": json.dumps(value)}]}}
91
+
92
+ responder = Responder(hub=_hub.HubClient(root, post=post), project_id=_FIXTURE_PROJECT, capabilities=["fixture.render"],
93
+ credential_file=cred, checkpoint_file=Path(tmp) / "checkpoint.json",
94
+ handler=lambda request, ctx: handled.append(request.body) or {"state": "completed", "summary": "Rendered."},
95
+ admit=lambda request: None, now=lambda: 0.0, rand=lambda: 0.5, spawn=lambda fn: fn(),
96
+ session_factory=lambda grant: BoardSession(grant, post=post))
97
+ responder.step()
98
+ tools = [t for t, _ in calls]
99
+ if any(t not in RESPOND_TOOLS and t != "fabric.board.join" for t in tools):
100
+ problems.append("%s: a tool outside the respond scope was sent" % variant)
101
+ if variant == "whole":
102
+ if tools != ["fabric.board.join", "com.status", "com.enroll", "com.claim", "com.get", "com.accept", "com.complete"]:
103
+ problems.append("whole: the calls were %s" % ", ".join(tools))
104
+ accept = dict(calls).get("com.accept", {}).get("fence", {})
105
+ if (accept.get("digest"), accept.get("generation"), accept.get("attempt")) != (_FIXTURE_DIGEST, 2, "att_Zx9Cv8Bn7M"):
106
+ problems.append("whole: com.accept did not carry the claim's exact fence")
107
+ if handled != ["Render the fixture."]:
108
+ problems.append("whole: the handler did not see the request once")
109
+ if responder.checkpoint.entries:
110
+ problems.append("whole: the checkpoint kept a settled request")
111
+ elif variant == "fenced":
112
+ if handled or "com.complete" in tools:
113
+ problems.append("fenced: the kit kept acting after com.accept answered fenced")
114
+ elif "com.enroll" in tools or responder.state != "down":
115
+ problems.append("no-epoch: the kit did not fail closed without an idempotency epoch")
116
+ return problems
@@ -0,0 +1,162 @@
1
+ """The board link (DEC-0078, FAA-12b): the board session over the hub, the fsynced checkpoint and a claimed request as
2
+ data. Part of the board connector (FAA-65, layer L4), below the responder in fabric_board_session, which re-exports these
3
+ names. Standard library only, Python 3.9+."""
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import threading
8
+ from pathlib import Path
9
+ from typing import Any
10
+ from typing import Callable
11
+ from typing import Dict
12
+ from typing import Optional
13
+
14
+ import sys as _sys # noqa: E402
15
+ from pathlib import Path as _Path # noqa: E402
16
+ if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from any working directory (FAA-65)
17
+ _sys.path.insert(0, str(_Path(__file__).resolve().parent))
18
+ import fabric_hub as _hub
19
+ import fabric_service as _fs
20
+ from fabric_board_shapes import ( # noqa: E402 - the kit (FAA-65)
21
+ BoardError,
22
+ RESPOND_TOOLS,
23
+ refusal_problems,
24
+ )
25
+
26
+
27
+ class BoardSession:
28
+ """MCP over HTTP with a board session's one-session bearer: `open()` spends it with `initialize` and keeps the
29
+ `mcp-session-id`; `call(tool, args)` answers the tool's value or raises `BoardError`; `close()` sends the session's
30
+ DELETE, which releases every slot it holds (DEC-0073 §5). Thread-safe; the bearer never leaves the headers."""
31
+
32
+ def __init__(self, grant: _hub.BoardGrant, *, timeout: float = 15.0, post: Callable[..., Any] = _hub.post_mcp):
33
+ self.grant, self.timeout, self.post = grant, timeout, post
34
+ self.mcp_session: Optional[str] = None
35
+ self.protocol_version = _hub.MCP_PROTOCOL_VERSION
36
+ self._lock = threading.Lock()
37
+ self._rpc = 0
38
+
39
+ def _send(self, body: Dict[str, Any], method: str = "POST"):
40
+ try:
41
+ return self.post(self.grant.origin, body, self.grant.bearer(), session=self.mcp_session, timeout=self.timeout,
42
+ method=method, protocol_version=self.protocol_version if self.mcp_session else None)
43
+ except _hub.HubError as exc:
44
+ raise BoardError("unreachable", "Fabric's hub did not answer; a write may have been stored, so send it again with "
45
+ "the same idempotency key.", True) from exc
46
+
47
+ def open(self) -> None:
48
+ with self._lock:
49
+ self._rpc += 1
50
+ status, headers, parsed = self._send({"jsonrpc": "2.0", "id": self._rpc, "method": "initialize", "params": {
51
+ "protocolVersion": _hub.MCP_PROTOCOL_VERSION, "capabilities": {},
52
+ "clientInfo": {"name": "fabric-agent-adapter-board", "version": "1"}}})
53
+ if status == 401:
54
+ raise BoardError("session-ended", "Fabric refused the board session's bearer; join again.", True)
55
+ if status != 200 or not isinstance(parsed, dict) or not isinstance(parsed.get("result"), dict) or not headers.get("mcp-session-id"):
56
+ raise BoardError("unreachable", "Fabric did not open the board session (HTTP %d)." % status, True)
57
+ self.mcp_session = headers["mcp-session-id"]
58
+ version = parsed["result"].get("protocolVersion")
59
+ if isinstance(version, str):
60
+ self.protocol_version = version
61
+ self._send({"jsonrpc": "2.0", "method": "notifications/initialized"})
62
+
63
+ def call(self, tool: str, args: Dict[str, Any]) -> Dict[str, Any]:
64
+ if tool not in RESPOND_TOOLS:
65
+ raise BoardError("not-in-scope", "%s is not served to a board session; use a Project session for it." % tool, op=tool)
66
+ with self._lock:
67
+ if self.mcp_session is None:
68
+ raise BoardError("session-ended", "The board session is not open.", True, op=tool)
69
+ self._rpc += 1
70
+ status, _, parsed = self._send({"jsonrpc": "2.0", "id": self._rpc, "method": "tools/call",
71
+ "params": {"name": tool, "arguments": args}})
72
+ if status == 401:
73
+ raise BoardError("session-ended", "Fabric ended the board session (it left, was revoked or expired); join again.", True, op=tool)
74
+ try:
75
+ value = _hub.tool_answer(status, parsed, tool)
76
+ except _hub.HubError as exc:
77
+ raise BoardError("unreachable" if exc.retryable else "receipt-shape", exc.message, exc.retryable, op=tool) from None
78
+ if "error" in value:
79
+ problems = refusal_problems(value)
80
+ if problems:
81
+ raise BoardError("receipt-shape", "Fabric refused %s in a form the contract does not name." % tool, op=tool)
82
+ e = value["error"]
83
+ raise BoardError(e["code"], e["message"], e["retryable"], op=tool)
84
+ return value
85
+
86
+ def close(self) -> None:
87
+ with self._lock:
88
+ if self.mcp_session is None:
89
+ return
90
+ try:
91
+ self._send({}, method="DELETE")
92
+ except BoardError:
93
+ pass # best effort: the lease lapses in 60 s anyway
94
+ self.mcp_session = None
95
+
96
+
97
+ class Checkpoint:
98
+ """The responder's own journal: one JSON file (0600) holding every request it has claimed and not yet settled, each
99
+ with its fence and the idempotency key of the step in flight. Written atomically and fsynced BEFORE the step it
100
+ announces is sent; a write that fails raises `BoardError("checkpoint")` and the step is not taken."""
101
+
102
+ def __init__(self, path: Path):
103
+ self.path = Path(path)
104
+ self._lock = threading.Lock()
105
+ self.entries: Dict[str, Dict[str, Any]] = self._load()
106
+
107
+ def _load(self) -> Dict[str, Dict[str, Any]]:
108
+ try:
109
+ doc = json.loads(self.path.read_text(encoding="utf-8"))
110
+ except FileNotFoundError:
111
+ return {}
112
+ except (OSError, ValueError) as exc:
113
+ raise BoardError("checkpoint", "The board checkpoint %s cannot be read (%s); fix or move it before answering "
114
+ "requests." % (self.path, exc.__class__.__name__)) from None
115
+ entries = doc.get("requests") if isinstance(doc, dict) and doc.get("version") == 1 else None
116
+ if not isinstance(entries, dict):
117
+ raise BoardError("checkpoint", "The board checkpoint %s is not one this kit writes." % self.path)
118
+ return entries
119
+
120
+ def _write(self, entries: Dict[str, Dict[str, Any]]) -> None:
121
+ try:
122
+ _fs.ensure_private_dir(self.path.parent)
123
+ _fs.atomic_write(self.path, (json.dumps({"version": 1, "requests": entries}, indent=1, sort_keys=True) + "\n").encode())
124
+ except OSError as exc:
125
+ raise BoardError("checkpoint", "The board checkpoint could not be written (%s); nothing was sent." % (exc.strerror or exc.__class__.__name__)) from None
126
+
127
+ def put(self, entry: Dict[str, Any]) -> None:
128
+ with self._lock:
129
+ entries = dict(self.entries)
130
+ entries[entry["request"]] = json.loads(json.dumps(entry))
131
+ self._write(entries)
132
+ self.entries = entries
133
+
134
+ def drop(self, request_id: str) -> None:
135
+ with self._lock:
136
+ if request_id not in self.entries:
137
+ return
138
+ entries = dict(self.entries)
139
+ del entries[request_id]
140
+ self._write(entries)
141
+ self.entries = entries
142
+
143
+
144
+ class BoardRequest:
145
+ """A claimed request as data: never an instruction. `body` is the requester's text; `sender` its Project id and
146
+ principal (with provenance); `artifacts` its references, unverified."""
147
+
148
+ __slots__ = ("request_id", "message_id", "capability", "body", "body_format", "artifacts", "sender", "deadline", "thread")
149
+
150
+ def __init__(self, message: Dict[str, Any], capability: str):
151
+ req = message.get("request") or {}
152
+ body = message.get("body") or {}
153
+ self.request_id, self.message_id, self.capability = req.get("id"), message.get("id"), capability
154
+ self.body = body.get("text") if isinstance(body, dict) else None
155
+ self.body_format = body.get("format", "text/plain") if isinstance(body, dict) else "text/plain"
156
+ self.artifacts = list(message.get("artifacts") or [])
157
+ self.sender = message.get("sender") or {}
158
+ self.deadline = req.get("deadline")
159
+ self.thread = message.get("thread")
160
+
161
+ def __repr__(self) -> str: # never the body
162
+ return "BoardRequest(%r, %r)" % (self.request_id, self.capability)