@passioncode-ai/fabric-agent-adapter 0.10.0 → 0.11.1
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 +50 -0
- package/README.md +2 -2
- 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 +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +84 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +1 -1
- 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 +427 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +761 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +43 -10
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
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) 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]] = []
|
|
@@ -540,17 +551,39 @@ class McpToolServer:
|
|
|
540
551
|
name = params.get("name")
|
|
541
552
|
arguments = params.get("arguments") or {}
|
|
542
553
|
if name in ("fabric.job.get", "fabric.job.cancel") and self.jobs is not None:
|
|
543
|
-
|
|
554
|
+
verdict = None
|
|
555
|
+
if self._authorize is not None:
|
|
556
|
+
job_id = str(arguments.get("id", ""))
|
|
557
|
+
try:
|
|
558
|
+
private = self.jobs.private(job_id)
|
|
559
|
+
except UnknownJob:
|
|
560
|
+
return {"jsonrpc": "2.0", "id": rid, "result": unknown_job_result(job_id, traceparent=span)}
|
|
561
|
+
verdict = self._authorize(private["capability"], headers)
|
|
562
|
+
owner = private.get("delegation")
|
|
563
|
+
if verdict.get("ok") and owner and (owner.get("principal"), owner.get("caller")) != (verdict.get("principal"), verdict.get("caller")):
|
|
564
|
+
verdict = {"ok": False, "code": "scope-not-granted", "detail": "job-of-another-caller"}
|
|
565
|
+
if not verdict.get("ok"):
|
|
566
|
+
import fabric_delegation as _fd # noqa: E402 - only a gated server needs it
|
|
567
|
+
return {"jsonrpc": "2.0", "id": rid, "result": _fd.refusal_tool_result(verdict, traceparent=span)}
|
|
568
|
+
return {"jsonrpc": "2.0", "id": rid, "result": self._job_call(name, arguments, span, verdict)}
|
|
544
569
|
handler = self.handlers.get(name)
|
|
545
570
|
if handler is None:
|
|
546
571
|
return self._error(rid, -32602, "Unknown tool: %s." % name)
|
|
572
|
+
delegation = None
|
|
573
|
+
if self._authorize is not None:
|
|
574
|
+
verdict = self._authorize(name, headers)
|
|
575
|
+
if not verdict.get("ok"):
|
|
576
|
+
import fabric_delegation as _fd # noqa: E402 - only a gated server needs it
|
|
577
|
+
return {"jsonrpc": "2.0", "id": rid, "result": _fd.refusal_tool_result(verdict, traceparent=span)}
|
|
578
|
+
delegation = verdict
|
|
547
579
|
try:
|
|
548
|
-
structured = handler(arguments, CallContext(self, name, arguments, span))
|
|
580
|
+
structured = handler(arguments, CallContext(self, name, arguments, span, delegation))
|
|
549
581
|
except InteropError as exc:
|
|
550
582
|
return {"jsonrpc": "2.0", "id": rid, "result": tool_result({"error": {"code": "tool-error", "message": str(exc)}}, is_error=True, traceparent=span)}
|
|
551
583
|
return {"jsonrpc": "2.0", "id": rid, "result": tool_result(structured, traceparent=span)}
|
|
552
584
|
|
|
553
|
-
def _job_call(self, name: str, arguments: Dict[str, Any], span: str
|
|
585
|
+
def _job_call(self, name: str, arguments: Dict[str, Any], span: str,
|
|
586
|
+
delegation: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
554
587
|
job_id = str(arguments.get("id", ""))
|
|
555
588
|
try:
|
|
556
589
|
job_span = self.jobs.traceparent(job_id) or span
|
|
@@ -566,7 +599,7 @@ class McpToolServer:
|
|
|
566
599
|
matched = self.jobs.answer(job_id, responses)
|
|
567
600
|
if matched and self._on_input:
|
|
568
601
|
private = self.jobs.private(job_id)
|
|
569
|
-
self._on_input(job_id, matched, CallContext(self, private["capability"], private["input"], job_span))
|
|
602
|
+
self._on_input(job_id, matched, CallContext(self, private["capability"], private["input"], job_span, delegation))
|
|
570
603
|
return tool_result({"job": self.jobs.get(job_id)}, traceparent=job_span)
|
|
571
604
|
except UnknownJob:
|
|
572
605
|
return unknown_job_result(job_id, traceparent=span)
|
|
@@ -5,7 +5,7 @@ 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.11.1"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5"
|
|
11
11
|
---
|