@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_board_fixture.py
ADDED
|
@@ -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
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py
ADDED
|
@@ -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)
|