@passioncode-ai/fabric-agent-adapter 0.10.0 → 0.12.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 +92 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +6 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +98 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +8 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +492 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +865 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +47 -12
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py
CHANGED
|
@@ -279,14 +279,17 @@ class JobStore:
|
|
|
279
279
|
return dict(record["job"])
|
|
280
280
|
|
|
281
281
|
def create(self, capability: str, arguments: Any, traceparent: Optional[str] = None,
|
|
282
|
-
poll_interval_ms: Optional[int] = None) -> Dict[str, Any]:
|
|
282
|
+
poll_interval_ms: Optional[int] = None, delegation: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
283
283
|
"""Start a job; returns the handle a job-capable tool puts in structuredContent."""
|
|
284
284
|
job_id = "job_" + secrets.token_hex(12)
|
|
285
285
|
job: Dict[str, Any] = {"id": job_id, "status": "working"}
|
|
286
286
|
if poll_interval_ms:
|
|
287
287
|
job["pollIntervalMs"] = int(poll_interval_ms)
|
|
288
|
-
|
|
289
|
-
|
|
288
|
+
private: Dict[str, Any] = {"capability": capability, "input": arguments,
|
|
289
|
+
"traceparent": traceparent if parse_traceparent(traceparent) else None}
|
|
290
|
+
if delegation: # DEC-0039: who started it; only they read, cancel or answer it
|
|
291
|
+
private["delegation"] = {"principal": delegation.get("principal"), "caller": delegation.get("caller")}
|
|
292
|
+
self._save({"job": job, "private": private})
|
|
290
293
|
return {"job": {"id": job_id, "status": "working"}}
|
|
291
294
|
|
|
292
295
|
def get(self, job_id: str) -> Dict[str, Any]:
|
|
@@ -361,16 +364,19 @@ class JobStore:
|
|
|
361
364
|
class CallContext:
|
|
362
365
|
"""What a tool handler gets: the child span it runs in, and a way to start a job."""
|
|
363
366
|
|
|
364
|
-
def __init__(self, server: "McpToolServer", tool: str, arguments: Any, traceparent: str
|
|
367
|
+
def __init__(self, server: "McpToolServer", tool: str, arguments: Any, traceparent: str,
|
|
368
|
+
delegation: Optional[Dict[str, Any]] = None):
|
|
365
369
|
self.server = server
|
|
366
370
|
self.tool = tool
|
|
367
371
|
self.arguments = arguments
|
|
368
372
|
self.traceparent = traceparent
|
|
373
|
+
# DEC-0039: the accepted delegation (principal, caller, chain, claims) when the server has a gate.
|
|
374
|
+
self.delegation = delegation
|
|
369
375
|
|
|
370
376
|
def start_job(self, poll_interval_ms: Optional[int] = None) -> Dict[str, Any]:
|
|
371
377
|
if self.server.jobs is None:
|
|
372
378
|
raise InteropError("This server has no job store.")
|
|
373
|
-
handle = self.server.jobs.create(self.tool, self.arguments, self.traceparent, poll_interval_ms)
|
|
379
|
+
handle = self.server.jobs.create(self.tool, self.arguments, self.traceparent, poll_interval_ms, self.delegation)
|
|
374
380
|
self.server.job_started(handle["job"]["id"], self)
|
|
375
381
|
return handle
|
|
376
382
|
|
|
@@ -471,7 +477,12 @@ class McpToolServer:
|
|
|
471
477
|
|
|
472
478
|
def __init__(self, name: str, version: str, jobs: Optional[JobStore] = None,
|
|
473
479
|
on_job_started: Optional[Callable[[str, CallContext], None]] = None,
|
|
474
|
-
on_input: Optional[OnInput] = None
|
|
480
|
+
on_input: Optional[OnInput] = None,
|
|
481
|
+
authorize: Optional[Callable[[str, Any], Dict[str, Any]]] = None):
|
|
482
|
+
# authorize(tool, headers, tier=) runs before every handler (fabric_delegation.delegation_gate, DEC-0039):
|
|
483
|
+
# a refusal is answered in the DL-19 shape and the handler never runs. The built-in job tools are
|
|
484
|
+
# gated by the job's own capability, and answered only for the principal and caller that started it.
|
|
485
|
+
self._authorize = authorize
|
|
475
486
|
self.info = {"name": name, "version": version}
|
|
476
487
|
self.jobs = jobs
|
|
477
488
|
self.tools: List[Dict[str, Any]] = []
|
|
@@ -506,10 +517,12 @@ class McpToolServer:
|
|
|
506
517
|
def _error(rid: Any, code: int, message: str) -> Dict[str, Any]:
|
|
507
518
|
return {"jsonrpc": "2.0", "id": rid, "error": {"code": code, "message": message}}
|
|
508
519
|
|
|
509
|
-
def handle(self, message: Any, headers: Any = None) -> Optional[Dict[str, Any]]:
|
|
520
|
+
def handle(self, message: Any, headers: Any = None, tier: str = "T2") -> Optional[Dict[str, Any]]:
|
|
510
521
|
"""One JSON-RPC message. Pass the HTTP request headers so the 2026-07-28 standard headers
|
|
511
522
|
are checked (`mcp_header_problem`); a mismatch answers HEADER_MISMATCH even for a
|
|
512
|
-
notification, so the transport can return HTTP 400.
|
|
523
|
+
notification, so the transport can return HTTP 400. `tier` is the caller's origin for the
|
|
524
|
+
authorize gate (DL-25: T0 local and authenticated by the service's floor, T1 an enrolled
|
|
525
|
+
machine of the owner, T2 anything else); a gate never requires a token from T0 or T1."""
|
|
513
526
|
if not isinstance(message, dict) or message.get("jsonrpc") != "2.0" or not isinstance(message.get("method"), str):
|
|
514
527
|
return self._error(message.get("id") if isinstance(message, dict) else None, -32600, "Invalid request.")
|
|
515
528
|
problem = mcp_header_problem(message, headers)
|
|
@@ -540,17 +553,39 @@ class McpToolServer:
|
|
|
540
553
|
name = params.get("name")
|
|
541
554
|
arguments = params.get("arguments") or {}
|
|
542
555
|
if name in ("fabric.job.get", "fabric.job.cancel") and self.jobs is not None:
|
|
543
|
-
|
|
556
|
+
verdict = None
|
|
557
|
+
if self._authorize is not None:
|
|
558
|
+
job_id = str(arguments.get("id", ""))
|
|
559
|
+
try:
|
|
560
|
+
private = self.jobs.private(job_id)
|
|
561
|
+
except UnknownJob:
|
|
562
|
+
return {"jsonrpc": "2.0", "id": rid, "result": unknown_job_result(job_id, traceparent=span)}
|
|
563
|
+
verdict = self._authorize(private["capability"], headers, tier=tier)
|
|
564
|
+
owner = private.get("delegation")
|
|
565
|
+
if verdict.get("ok") and owner and (owner.get("principal"), owner.get("caller")) != (verdict.get("principal"), verdict.get("caller")):
|
|
566
|
+
verdict = {"ok": False, "code": "scope-not-granted", "detail": "job-of-another-caller"}
|
|
567
|
+
if not verdict.get("ok"):
|
|
568
|
+
import fabric_delegation as _fd # noqa: E402 - only a gated server needs it
|
|
569
|
+
return {"jsonrpc": "2.0", "id": rid, "result": _fd.refusal_tool_result(verdict, traceparent=span)}
|
|
570
|
+
return {"jsonrpc": "2.0", "id": rid, "result": self._job_call(name, arguments, span, verdict)}
|
|
544
571
|
handler = self.handlers.get(name)
|
|
545
572
|
if handler is None:
|
|
546
573
|
return self._error(rid, -32602, "Unknown tool: %s." % name)
|
|
574
|
+
delegation = None
|
|
575
|
+
if self._authorize is not None:
|
|
576
|
+
verdict = self._authorize(name, headers, tier=tier)
|
|
577
|
+
if not verdict.get("ok"):
|
|
578
|
+
import fabric_delegation as _fd # noqa: E402 - only a gated server needs it
|
|
579
|
+
return {"jsonrpc": "2.0", "id": rid, "result": _fd.refusal_tool_result(verdict, traceparent=span)}
|
|
580
|
+
delegation = verdict
|
|
547
581
|
try:
|
|
548
|
-
structured = handler(arguments, CallContext(self, name, arguments, span))
|
|
582
|
+
structured = handler(arguments, CallContext(self, name, arguments, span, delegation))
|
|
549
583
|
except InteropError as exc:
|
|
550
584
|
return {"jsonrpc": "2.0", "id": rid, "result": tool_result({"error": {"code": "tool-error", "message": str(exc)}}, is_error=True, traceparent=span)}
|
|
551
585
|
return {"jsonrpc": "2.0", "id": rid, "result": tool_result(structured, traceparent=span)}
|
|
552
586
|
|
|
553
|
-
def _job_call(self, name: str, arguments: Dict[str, Any], span: str
|
|
587
|
+
def _job_call(self, name: str, arguments: Dict[str, Any], span: str,
|
|
588
|
+
delegation: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
554
589
|
job_id = str(arguments.get("id", ""))
|
|
555
590
|
try:
|
|
556
591
|
job_span = self.jobs.traceparent(job_id) or span
|
|
@@ -566,7 +601,7 @@ class McpToolServer:
|
|
|
566
601
|
matched = self.jobs.answer(job_id, responses)
|
|
567
602
|
if matched and self._on_input:
|
|
568
603
|
private = self.jobs.private(job_id)
|
|
569
|
-
self._on_input(job_id, matched, CallContext(self, private["capability"], private["input"], job_span))
|
|
604
|
+
self._on_input(job_id, matched, CallContext(self, private["capability"], private["input"], job_span, delegation))
|
|
570
605
|
return tool_result({"job": self.jobs.get(job_id)}, traceparent=job_span)
|
|
571
606
|
except UnknownJob:
|
|
572
607
|
return unknown_job_result(job_id, traceparent=span)
|
|
@@ -5,9 +5,9 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode. Ships in one plugin with adapting-projects-to-fabric, whose scripts it reuses.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.12.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
|
-
contract-commit: "
|
|
10
|
+
contract-commit: "2eb91c00e852d9c31d804310d38fad91589fcbac"
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Creating Fabric-compatible agents
|
|
@@ -119,7 +119,7 @@ python3 <plugin-dir>/skills/adapting-projects-to-fabric/scripts/adapt_project.py
|
|
|
119
119
|
--schema-base <immutable-base-uri>
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
Pin exactly contract `0.1.0` at commit `
|
|
122
|
+
Pin exactly contract `0.1.0` at commit `2eb91c00e852d9c31d804310d38fad91589fcbac` and
|
|
123
123
|
read the pinned guide before implementing protocol details. If this skill is installed
|
|
124
124
|
without its sibling, the scaffolder is absent: create the bundle by hand from the pinned
|
|
125
125
|
contract's `docs/guides/connecting-compatible-agents.md` and mark the structural check
|