@passioncode-ai/fabric-agent-adapter 0.82.0 → 0.84.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 +169 -3
- package/README.md +7 -3
- package/SECURITY.md +4 -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 +7 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +18 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +16 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_identifiers.py +1 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +31 -15
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +38 -12
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +21 -148
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check_cli.py +164 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +15 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +4 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +14 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/README.md +76 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/config.example.json +28 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/data_agent.py +366 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +32 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +4 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support-agent.mjs +115 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +63 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/a2a.md +98 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +28 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +13 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +11 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/ops-digest.md +75 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +7 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +10 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +70 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_a2a.py +158 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a-server.mjs +275 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a.mjs +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +55 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +47 -31
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +25 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +27 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +19 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate-rules.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +7 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +22 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +4 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-identifiers.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +21 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +25 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ops-digest.mjs +456 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package-schema.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +18 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-page-links.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +19 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +56 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-socket-proof.mjs +181 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +20 -15
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet-identity.mjs +469 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a.py +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a_server.py +405 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +17 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +98 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +37 -48
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +25 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +28 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +19 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +24 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate_rules.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +32 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +19 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +10 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_identifiers.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +56 -96
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +99 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +16 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_mandate.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +44 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ops_digest.py +648 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +17 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package_schema.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_page_links.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +21 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +108 -108
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +38 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_socket_proof.py +391 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +19 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet_identity.py +493 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +632 -104
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample-remote-service.mjs +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +17 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/tailnet-groups.schema.json +69 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +6 -5
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +16 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +14 -9
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +4 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +18 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +4 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/admit.py +182 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +14 -9
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +7 -4
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +10 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +2 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +16 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +79 -8
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +147 -45
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
2
3
|
"""Reference kit for the fabric-interop/0.1 extension: how agents are called.
|
|
3
4
|
|
|
4
5
|
Standard library only, Python 3.9+. It sits beside fabric_service.py and uses it for
|
|
@@ -17,7 +18,6 @@ Normative source: fabric-agent-contract docs/specification/interop.md (DEC-0016,
|
|
|
17
18
|
|
|
18
19
|
from __future__ import annotations
|
|
19
20
|
|
|
20
|
-
import base64
|
|
21
21
|
import hashlib
|
|
22
22
|
import json
|
|
23
23
|
import math
|
|
@@ -98,6 +98,14 @@ from fabric_interop_rules import ( # noqa: E402,F401 - the rule layer (FAA-92)
|
|
|
98
98
|
pause_message,
|
|
99
99
|
intent_digest,
|
|
100
100
|
repeated_key_decision,
|
|
101
|
+
NAMED_METHODS,
|
|
102
|
+
UNREADABLE_AGENT,
|
|
103
|
+
identified_agent,
|
|
104
|
+
HEADER_MISMATCH,
|
|
105
|
+
encode_header_value,
|
|
106
|
+
decode_header_value,
|
|
107
|
+
mcp_header_problem,
|
|
108
|
+
mcp_request_headers,
|
|
101
109
|
)
|
|
102
110
|
|
|
103
111
|
# --- C3.1 capabilities as tools --------------------------------------------------
|
|
@@ -399,6 +407,8 @@ class JobStore:
|
|
|
399
407
|
A step is kept as the gate reads it (kept_step), so a job's start and its answer agree."""
|
|
400
408
|
if not isinstance(input_requests, dict) or not input_requests: # a JSON object of requests, first (readings 38, 39)
|
|
401
409
|
raise InteropError("input_required needs a JSON object of at least one input request.")
|
|
410
|
+
if steps is not None and not isinstance(steps, dict): # a JSON object or none, in both kits (FAA-196 (26))
|
|
411
|
+
raise InteropError("steps maps an input request's key to its approval step: a JSON object, or none.")
|
|
402
412
|
kept: Dict[str, Dict[str, Any]] = {}
|
|
403
413
|
for key, step in (steps or {}).items(): # the value checked is the value kept (seam reading 34)
|
|
404
414
|
kept[key] = kept_step(step) if key in input_requests and isinstance(step, dict) else {}
|
|
@@ -493,7 +503,8 @@ class CallContext:
|
|
|
493
503
|
def __init__(self, server: "McpToolServer", tool: str, arguments: Any, traceparent: str,
|
|
494
504
|
delegation: Optional[Dict[str, Any]] = None, *, tier: Optional[str] = None, caller: Optional[str] = None,
|
|
495
505
|
meta: Optional[Dict[str, Any]] = None, record: Any = None, decision: Optional[Dict[str, Any]] = None,
|
|
496
|
-
input_records: Optional[Dict[str, Any]] = None, mandate: Optional[Dict[str, Any]] = None
|
|
506
|
+
input_records: Optional[Dict[str, Any]] = None, mandate: Optional[Dict[str, Any]] = None,
|
|
507
|
+
caller_agent: Optional[str] = None):
|
|
497
508
|
self.server = server
|
|
498
509
|
self.tool = tool
|
|
499
510
|
self.arguments = arguments
|
|
@@ -503,12 +514,19 @@ class CallContext:
|
|
|
503
514
|
self.tier, self.caller, self.meta = tier, caller, {} if meta is None else meta
|
|
504
515
|
self.record, self.decision, self.input_records = record, decision, input_records
|
|
505
516
|
self.mandate = mandate
|
|
517
|
+
# FAA-135: the caller's agent id the service authenticated (the delegation's caller, else identify_agent's).
|
|
518
|
+
delegated = (delegation or {}).get("caller")
|
|
519
|
+
self.caller_agent = caller_agent if caller_agent is not None else (delegated if isinstance(delegated, str) and delegated else None)
|
|
506
520
|
|
|
507
521
|
def start_job(self, poll_interval_ms: Optional[int] = None) -> Dict[str, Any]:
|
|
508
522
|
if self.server.jobs is None:
|
|
509
523
|
raise InteropError("This server has no job store.")
|
|
510
524
|
handle = self.server.jobs.create(self.tool, self.arguments, self.traceparent, poll_interval_ms, self.delegation)
|
|
511
|
-
|
|
525
|
+
try:
|
|
526
|
+
self.server.job_started(handle["job"]["id"], self)
|
|
527
|
+
except InteropError as exc: # a refused start: the job it created ends with it, never left working (FAA-196 (28))
|
|
528
|
+
self.server.jobs.fail(handle["job"]["id"], "tool-error", str(exc)[:_MAX_ERROR_MESSAGE] or "The job's start was refused.")
|
|
529
|
+
raise
|
|
512
530
|
return handle
|
|
513
531
|
|
|
514
532
|
def approval_step(self, job_id: str, key: str, request: Dict[str, Any], *, effect: str, spend_usd: Any = None,
|
|
@@ -527,7 +545,7 @@ class CallContext:
|
|
|
527
545
|
stored = self.server.jobs.private(job_id).get("capability")
|
|
528
546
|
capability = stored if isinstance(stored, str) else self.tool
|
|
529
547
|
if self.server.agent_id is not None:
|
|
530
|
-
caller =
|
|
548
|
+
caller = self.caller_agent
|
|
531
549
|
carried = self.meta.get("mandate")
|
|
532
550
|
if carried is None and "mandate" in self.meta:
|
|
533
551
|
carried = {} # a null mandate is present and unreadable, as the answer reads it; never "none" (FAA-178)
|
|
@@ -550,84 +568,6 @@ JOB_REQUEST_SCHEMA = {"type": "object", "required": ["id"], "additionalPropertie
|
|
|
550
568
|
"inputResponses": {"type": "object"}}}
|
|
551
569
|
|
|
552
570
|
|
|
553
|
-
NAMED_METHODS = {"tools/call": "name", "prompts/get": "name", "resources/read": "uri"}
|
|
554
|
-
HEADER_MISMATCH = -32020 # the code the official 2026-07-28 server SDK answers a header/body mismatch with
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
_SENTINEL = ("=?base64?", "?=")
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
def encode_header_value(value: str) -> str:
|
|
561
|
-
"""A body value as an HTTP header value, by MCP 2026-07-28 Streamable HTTP "Value Encoding": as
|
|
562
|
-
it is when it is printable ASCII without leading or trailing whitespace, otherwise — and for a
|
|
563
|
-
plain value that itself looks like the sentinel — `=?base64?<base64 of its UTF-8>?=`."""
|
|
564
|
-
plain = (all(0x20 <= ord(c) <= 0x7E for c in value) and value == value.strip(" ")
|
|
565
|
-
and not (value.startswith(_SENTINEL[0]) and value.endswith(_SENTINEL[1])))
|
|
566
|
-
if plain:
|
|
567
|
-
return value
|
|
568
|
-
return _SENTINEL[0] + base64.b64encode(value.encode("utf-8")).decode("ascii") + _SENTINEL[1]
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
def decode_header_value(value: str) -> Optional[str]:
|
|
572
|
-
"""The body value a header carries, decoding the Base64 sentinel; None for a header value with
|
|
573
|
-
invalid characters or a sentinel that is not valid Base64 of UTF-8 (a server rejects both)."""
|
|
574
|
-
if value.startswith(_SENTINEL[0]) and value.endswith(_SENTINEL[1]) and len(value) >= len(_SENTINEL[0]) + len(_SENTINEL[1]):
|
|
575
|
-
try:
|
|
576
|
-
return base64.b64decode(value[len(_SENTINEL[0]):-len(_SENTINEL[1])], validate=True).decode("utf-8")
|
|
577
|
-
except ValueError: # binascii.Error and UnicodeDecodeError are both ValueErrors
|
|
578
|
-
return None
|
|
579
|
-
if any(not (0x20 <= ord(c) <= 0x7E or c == "\t") for c in value):
|
|
580
|
-
return None
|
|
581
|
-
return value
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
def mcp_header_problem(message: Any, headers: Any) -> Optional[str]:
|
|
585
|
-
"""MCP 2026-07-28 Streamable HTTP mirrors the body in `Mcp-Method` (every request and
|
|
586
|
-
notification) and `Mcp-Name` (`params.name` or `params.uri` of tools/call, prompts/get,
|
|
587
|
-
resources/read). A server MUST reject a mismatch; a request that declares 2026-07-28 must
|
|
588
|
-
carry them. Earlier revisions may omit them, but what they send must still agree.
|
|
589
|
-
`headers` is any case-insensitive mapping with `.get` (http.server's message, a dict)."""
|
|
590
|
-
if not isinstance(message, dict) or headers is None:
|
|
591
|
-
return None
|
|
592
|
-
def header(name: str) -> Optional[str]:
|
|
593
|
-
value = headers.get(name) if hasattr(headers, "get") else None
|
|
594
|
-
if value is None and isinstance(headers, dict):
|
|
595
|
-
value = next((v for k, v in headers.items() if k.lower() == name.lower()), None)
|
|
596
|
-
if isinstance(value, (list, tuple)):
|
|
597
|
-
value = value[0] if value else None
|
|
598
|
-
return None if value is None else str(value)
|
|
599
|
-
declared = header("MCP-Protocol-Version") == MCP_REVISION
|
|
600
|
-
method = message.get("method")
|
|
601
|
-
sent = header("Mcp-Method")
|
|
602
|
-
if sent is None and declared:
|
|
603
|
-
return "the body names method %s but the required Mcp-Method header is absent" % method
|
|
604
|
-
if sent is not None and sent != method:
|
|
605
|
-
return "Mcp-Method %s disagrees with the body's method %s" % (sent, method)
|
|
606
|
-
field = NAMED_METHODS.get(str(method))
|
|
607
|
-
if field:
|
|
608
|
-
params = message.get("params")
|
|
609
|
-
value = params.get(field) if isinstance(params, dict) else None
|
|
610
|
-
named = header("Mcp-Name")
|
|
611
|
-
if named is None and declared:
|
|
612
|
-
return "the body carries params.%s but the required Mcp-Name header is absent" % field
|
|
613
|
-
if named is not None:
|
|
614
|
-
decoded = decode_header_value(named)
|
|
615
|
-
if decoded is None:
|
|
616
|
-
return "Mcp-Name is malformed: invalid characters or a broken =?base64?…?= value"
|
|
617
|
-
if decoded != str(value):
|
|
618
|
-
return "Mcp-Name disagrees with the body's params.%s" % field
|
|
619
|
-
return None
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
def mcp_request_headers(method: str, params: Dict[str, Any]) -> Dict[str, str]:
|
|
623
|
-
"""The standard headers a 2026-07-28 client sends with one request."""
|
|
624
|
-
out = {"MCP-Protocol-Version": MCP_REVISION, "Mcp-Method": method}
|
|
625
|
-
field = NAMED_METHODS.get(method)
|
|
626
|
-
if field and params.get(field) is not None:
|
|
627
|
-
out["Mcp-Name"] = encode_header_value(str(params[field]))
|
|
628
|
-
return out
|
|
629
|
-
|
|
630
|
-
|
|
631
571
|
_UNREADABLE = object() # what a service's spend or project function answers when it raised
|
|
632
572
|
_UNREADABLE_PROJECT = UNREADABLE_PROJECT # never a member of a mandate's projects, so a scoped mandate refuses
|
|
633
573
|
|
|
@@ -649,7 +589,8 @@ class McpToolServer:
|
|
|
649
589
|
approve: Optional[Callable[[str, Dict[str, Any], Any, str], Optional[Dict[str, Any]]]] = None,
|
|
650
590
|
elicited: Optional[Callable[[str, Dict[str, Any], Optional[str]], bool]] = None,
|
|
651
591
|
input_actions: bool = False, clock: Callable[[], float] = time.time,
|
|
652
|
-
on_fault: Optional[Callable[[str, str], None]] = None, agent_id: Optional[str] = None
|
|
592
|
+
on_fault: Optional[Callable[[str, str], None]] = None, agent_id: Optional[str] = None,
|
|
593
|
+
identify_agent: Optional[Callable[[Any, str], Optional[str]]] = None):
|
|
653
594
|
# authorize(tool, headers, tier=) runs before every handler (fabric_delegation.delegation_gate, DEC-0039):
|
|
654
595
|
# a refusal is answered in the DL-19 shape and the handler never runs. The built-in job tools are
|
|
655
596
|
# gated by the job's own capability, and answered only for the principal and caller that started it.
|
|
@@ -663,12 +604,14 @@ class McpToolServer:
|
|
|
663
604
|
# (MJ-1), which the caller reads as operator-decision-required naming it in data.detail.
|
|
664
605
|
# DEC-0125: agent_id is this service's own agent id; with it the server reads a call's _meta.mandate for a
|
|
665
606
|
# relayed action added with its effect (the chain must end in agent_id, and in the delegation's caller before
|
|
666
|
-
# it when authorize accepted a token); without it a mandate is not read, as before.
|
|
607
|
+
# it when authorize accepted a token); without it a mandate is not read, as before. FAA-135:
|
|
608
|
+
# identify_agent(headers, tier) names the agent id of a per-agent credential the service authenticated, read
|
|
609
|
+
# when no delegation named a caller: the chain must hold it before agent_id (identified_agent reads its answer).
|
|
667
610
|
if agent_id is not None and not is_agent_id(agent_id):
|
|
668
611
|
raise InteropError("agent_id is this service's agent id (a lower-case id, optionally .instance; DEC-0125).")
|
|
669
612
|
self.agent_id = agent_id
|
|
670
613
|
self._authorize = authorize
|
|
671
|
-
self._identify, self._approve, self._elicited = identify, approve, elicited
|
|
614
|
+
self._identify, self._approve, self._elicited, self._identify_agent = identify, approve, elicited, identify_agent
|
|
672
615
|
self.input_actions, self._clock, self._on_fault = input_actions is True, clock, on_fault
|
|
673
616
|
self.actions: Dict[str, Dict[str, Any]] = {}
|
|
674
617
|
self.info = {"name": name, "version": version}
|
|
@@ -739,6 +682,20 @@ class McpToolServer:
|
|
|
739
682
|
authenticated = self._identify(headers, tier)
|
|
740
683
|
return action_caller(tier, authenticated)
|
|
741
684
|
|
|
685
|
+
def _caller_agent(self, headers: Any, tier: str, verdict: Optional[Dict[str, Any]]) -> Optional[str]:
|
|
686
|
+
"""FAA-135: the caller's agent id a mandate's chain is bound with: the delegation's caller when authorize accepted
|
|
687
|
+
a token, else what identify_agent names (identified_agent; a hook that raises names UNREADABLE_AGENT), else
|
|
688
|
+
None. Read only on a server with agent_id; never a name the call carries."""
|
|
689
|
+
delegated = (verdict or {}).get("caller")
|
|
690
|
+
if self.agent_id is None or (isinstance(delegated, str) and delegated):
|
|
691
|
+
return delegated if self.agent_id is not None else None
|
|
692
|
+
if self._identify_agent is None:
|
|
693
|
+
return None
|
|
694
|
+
try:
|
|
695
|
+
return identified_agent(self._identify_agent(headers, tier))
|
|
696
|
+
except Exception: # noqa: BLE001 - the service's own hook: an id no chain holds, so a mandate is refused
|
|
697
|
+
return UNREADABLE_AGENT
|
|
698
|
+
|
|
742
699
|
def _gate_action(self, action: Dict[str, Any], name: str, arguments: Dict[str, Any], target: Any, url: Any,
|
|
743
700
|
headers: Any, tier: str, meta: Dict[str, Any], caller: Optional[str],
|
|
744
701
|
mandate: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
@@ -766,13 +723,12 @@ class McpToolServer:
|
|
|
766
723
|
return action_gate(action, tier=tier, caller=caller, meta=meta, target=target, elicited=elicited, approval=approval,
|
|
767
724
|
action_digest=digest, human_step_url=url, now=format_rfc3339(self._clock() * 1000), **(mandate or {}))
|
|
768
725
|
|
|
769
|
-
def _mandate_inputs(self, entry: Dict[str, Any], arguments: Dict[str, Any],
|
|
726
|
+
def _mandate_inputs(self, entry: Dict[str, Any], arguments: Dict[str, Any], caller_agent: Optional[str]) -> Dict[str, Any]:
|
|
770
727
|
"""DEC-0125: what action_gate reads a mandate with, empty on a server without agent_id. The caller's agent id is
|
|
771
|
-
the delegation's caller
|
|
728
|
+
_caller_agent's (the delegation's caller, else identify_agent's, FAA-135), never a name the call carries."""
|
|
772
729
|
if self.agent_id is None:
|
|
773
730
|
return {}
|
|
774
731
|
spend, project = (self._read(entry.get(k), arguments) for k in ("spend", "project"))
|
|
775
|
-
caller_agent = (verdict or {}).get("caller")
|
|
776
732
|
# The estimate is read as a double, as the contract does: an int past the float range is infinite, and an
|
|
777
733
|
# estimate that is not a finite number reads as none, which a mandate's ceiling refuses (fail closed).
|
|
778
734
|
number = _real_number(spend) # by what it stores; a bool, a mock or not a number is none (seam reading 18)
|
|
@@ -799,7 +755,7 @@ class McpToolServer:
|
|
|
799
755
|
return _UNREADABLE
|
|
800
756
|
|
|
801
757
|
def _gate_inputs(self, arguments: Dict[str, Any], headers: Any, tier: str, meta: Dict[str, Any],
|
|
802
|
-
caller: Optional[str],
|
|
758
|
+
caller: Optional[str], agent: Optional[str] = None) -> Dict[str, Any]:
|
|
803
759
|
"""D8: each input request an answer would take is an action (id its key, tool fabric.job.get, gate relayed),
|
|
804
760
|
one decision per call; the first refusal refuses the call and nothing is applied. DEC-0125: on a server with
|
|
805
761
|
agent_id, a step approval_step kept is read under the call's `_meta.mandate` too (C3.3)."""
|
|
@@ -818,7 +774,6 @@ class McpToolServer:
|
|
|
818
774
|
# A kept step's mandate is read as approval_step read it, under the job's own capability, which must
|
|
819
775
|
# be a capability name (MJ-1); the action itself stays fabric.job.get, so it is a readable action.
|
|
820
776
|
capability = private.get("capability")
|
|
821
|
-
agent = (verdict or {}).get("caller")
|
|
822
777
|
mandate = {"self_id": self.agent_id, "effect": step.get("effect"), "spend_usd": step.get("spendUsd"),
|
|
823
778
|
"project": step.get("project"), "caller_agent": agent if isinstance(agent, str) and agent else None,
|
|
824
779
|
"mandate_capability": capability if isinstance(capability, str) else None}
|
|
@@ -921,13 +876,13 @@ class McpToolServer:
|
|
|
921
876
|
if not verdict.get("ok"):
|
|
922
877
|
import fabric_delegation as _fd # noqa: E402 - only a gated server needs it
|
|
923
878
|
return {"jsonrpc": "2.0", "id": rid, "result": _fd.refusal_tool_result(verdict, traceparent=span)}
|
|
924
|
-
caller, inputs = self._caller(headers, tier, verdict), None
|
|
879
|
+
caller, agent, inputs = self._caller(headers, tier, verdict), self._caller_agent(headers, tier, verdict), None
|
|
925
880
|
responses = arguments.get("inputResponses")
|
|
926
881
|
if name == "fabric.job.get" and responses is not None and not isinstance(responses, dict): # seam r37: never a crash
|
|
927
882
|
return self._error(rid, -32602, "fabric.job.get takes inputResponses as a JSON object (interop C3.3).")
|
|
928
883
|
if name == "fabric.job.get" and self.input_actions and isinstance(responses, dict) and responses:
|
|
929
884
|
try:
|
|
930
|
-
inputs = self._gate_inputs(arguments, headers, tier, meta, caller,
|
|
885
|
+
inputs = self._gate_inputs(arguments, headers, tier, meta, caller, agent)
|
|
931
886
|
except UnknownJob:
|
|
932
887
|
return {"jsonrpc": "2.0", "id": rid, "result": unknown_job_result(str(arguments.get("id", "")), traceparent=span)}
|
|
933
888
|
if not inputs["ok"] and "invalid" in inputs:
|
|
@@ -937,7 +892,8 @@ class McpToolServer:
|
|
|
937
892
|
self._fault(stored if isinstance(stored, str) else name, inputs) # as the start heard it (FAA-179)
|
|
938
893
|
import fabric_delegation as _fd # noqa: E402 - DL-19
|
|
939
894
|
return {"jsonrpc": "2.0", "id": rid, "result": _fd.refusal_tool_result(inputs["refusal"], traceparent=span)}
|
|
940
|
-
context = {"tier": tier, "caller": caller, "meta": meta, "input_records": inputs["records"] if inputs else None
|
|
895
|
+
context = {"tier": tier, "caller": caller, "meta": meta, "input_records": inputs["records"] if inputs else None,
|
|
896
|
+
"caller_agent": agent}
|
|
941
897
|
return {"jsonrpc": "2.0", "id": rid, "result": self._job_call(name, arguments, span, verdict, context)}
|
|
942
898
|
handler = self.handlers.get(name)
|
|
943
899
|
if handler is None:
|
|
@@ -950,9 +906,10 @@ class McpToolServer:
|
|
|
950
906
|
return {"jsonrpc": "2.0", "id": rid, "result": _fd.refusal_tool_result(verdict, traceparent=span)}
|
|
951
907
|
delegation = verdict
|
|
952
908
|
caller, gated, entry = self._caller(headers, tier, delegation), {"record": None, "decision": None}, self.actions.get(name)
|
|
909
|
+
agent = self._caller_agent(headers, tier, delegation)
|
|
953
910
|
if entry is not None: # DEC-0121: after the authorize gate, before the handler
|
|
954
911
|
gated = self._gate_action(entry["action"], name, arguments, self._target(entry["target"], arguments), entry["url"],
|
|
955
|
-
headers, tier, meta, caller, self._mandate_inputs(entry, arguments,
|
|
912
|
+
headers, tier, meta, caller, self._mandate_inputs(entry, arguments, agent))
|
|
956
913
|
if not gated["ok"] and "invalid" in gated:
|
|
957
914
|
return self._error(rid, -32602, gated["invalid"])
|
|
958
915
|
if not gated["ok"]:
|
|
@@ -960,7 +917,7 @@ class McpToolServer:
|
|
|
960
917
|
import fabric_delegation as _fd # noqa: E402 - DL-19
|
|
961
918
|
return {"jsonrpc": "2.0", "id": rid, "result": _fd.refusal_tool_result(gated["refusal"], traceparent=span)}
|
|
962
919
|
context = CallContext(self, name, arguments, span, delegation, tier=tier, caller=caller, meta=meta,
|
|
963
|
-
record=gated["record"], decision=gated["decision"], mandate=gated.get("mandate"))
|
|
920
|
+
record=gated["record"], decision=gated["decision"], mandate=gated.get("mandate"), caller_agent=agent)
|
|
964
921
|
try:
|
|
965
922
|
structured = handler(arguments, context)
|
|
966
923
|
except InteropError as exc:
|
|
@@ -990,7 +947,10 @@ class McpToolServer:
|
|
|
990
947
|
matched = self.jobs.answer(job_id, responses)
|
|
991
948
|
if matched and self._on_input:
|
|
992
949
|
private = self.jobs.private(job_id)
|
|
993
|
-
|
|
950
|
+
try:
|
|
951
|
+
self._on_input(job_id, matched, CallContext(self, private["capability"], private["input"], job_span, delegation, **context))
|
|
952
|
+
except InteropError as exc: # answered as the handler path answers one (FAA-196 (27))
|
|
953
|
+
return tool_result({"error": {"code": "tool-error", "message": str(exc)}}, is_error=True, traceparent=job_span)
|
|
994
954
|
return tool_result({"job": self.jobs.get(job_id)}, traceparent=job_span)
|
|
995
955
|
except UnknownJob:
|
|
996
956
|
return unknown_job_result(job_id, traceparent=span)
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""fabric-interop/0.1, the rule layer: the extension's names and limits, its errors, the trace context (C3.4), a
|
|
2
3
|
usage object's reading (C3.2) and the job limits and uncertain effects (DEC-0087). Every function answers by a rule
|
|
3
4
|
and touches no file; fabric_interop builds the tools, the envelope, the job store and the MCP server on it and
|
|
@@ -5,6 +6,7 @@ re-exports every name here. Part of the service kit (FAA-92, split from fabric_i
|
|
|
5
6
|
only, Python 3.9+."""
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
9
|
+
import base64
|
|
8
10
|
import hashlib
|
|
9
11
|
import json
|
|
10
12
|
import math
|
|
@@ -20,7 +22,7 @@ if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from
|
|
|
20
22
|
from fabric_base import ServiceError # noqa: E402
|
|
21
23
|
from fabric_actions import action_decision, decision_shape, job_capability # noqa: E402 - DEC-0121
|
|
22
24
|
from fabric_mandate import ( # noqa: E402 - DEC-0125, DEC-0126
|
|
23
|
-
EFFECT_CLASSES, OPERATOR_SOURCE_PROTOCOL, mandate_covers, mandate_problem, without_source)
|
|
25
|
+
EFFECT_CLASSES, OPERATOR_SOURCE_PROTOCOL, is_agent_id, mandate_covers, mandate_problem, without_source)
|
|
24
26
|
|
|
25
27
|
PROTOCOL = "fabric-interop/0.1"
|
|
26
28
|
EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
|
|
@@ -377,6 +379,22 @@ def job_step(mandate: Any, *, tier: Any, now: Any, self_id: Any, capability: Any
|
|
|
377
379
|
UNREADABLE_PROJECT = "\x00unreadable"
|
|
378
380
|
|
|
379
381
|
|
|
382
|
+
#: The caller's agent id when a service's identify_agent hook answered something no chain can hold (an id the registry
|
|
383
|
+
#: refuses, not a string, or the hook raised): passed as callerAgent it matches no agent, so every mandate reads
|
|
384
|
+
#: mandate-not-for-this-call (fail closed, FAA-135). Never an agent id (`is_agent_id` refuses it).
|
|
385
|
+
UNREADABLE_AGENT = "\x00unreadable-agent"
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def identified_agent(value: Any) -> Optional[str]:
|
|
389
|
+
"""FAA-135: the caller's agent id as a service's own hook named it from a per-agent credential (contract delegation.md,
|
|
390
|
+
`surfaces.mcp.auth: own`), read as the mandate chain binds it (`callerAgent`): None (no per-agent credential) is
|
|
391
|
+
none, and the chain's end alone binds the mandate, as before; an agent id the registry accepts is that id; anything
|
|
392
|
+
else is UNREADABLE_AGENT, which no chain holds. Never a name the call itself carries."""
|
|
393
|
+
if value is None:
|
|
394
|
+
return None
|
|
395
|
+
return value if is_agent_id(value) else UNREADABLE_AGENT
|
|
396
|
+
|
|
397
|
+
|
|
380
398
|
def mandate_spend(value: Any) -> Any:
|
|
381
399
|
"""A spend estimate as a mandate reads it (FAA-139): a number read as a double, as the contract does; one that is
|
|
382
400
|
not a finite double (an int past the float range, NaN, infinity) or not a number is none, which a mandate's
|
|
@@ -559,3 +577,83 @@ def repeated_key_decision(job: Any, now: Any, limit_holds: Optional[bool] = None
|
|
|
559
577
|
return "same"
|
|
560
578
|
return "run" if "resetsAt" not in data or utc_time(data["resetsAt"]) <= at else "same"
|
|
561
579
|
# #endregion job-limits-kit
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
# --- MCP 2026-07-28 header mirroring (moved from fabric_interop in 0.83.0 to make room under the line ceiling) ---
|
|
583
|
+
|
|
584
|
+
NAMED_METHODS = {"tools/call": "name", "prompts/get": "name", "resources/read": "uri"}
|
|
585
|
+
HEADER_MISMATCH = -32020 # the code the official 2026-07-28 server SDK answers a header/body mismatch with
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
_SENTINEL = ("=?base64?", "?=")
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def encode_header_value(value: str) -> str:
|
|
592
|
+
"""A body value as an HTTP header value, by MCP 2026-07-28 Streamable HTTP "Value Encoding": as
|
|
593
|
+
it is when it is printable ASCII without leading or trailing whitespace, otherwise (and for a
|
|
594
|
+
plain value that itself looks like the sentinel) `=?base64?<base64 of its UTF-8>?=`."""
|
|
595
|
+
plain = (all(0x20 <= ord(c) <= 0x7E for c in value) and value == value.strip(" ")
|
|
596
|
+
and not (value.startswith(_SENTINEL[0]) and value.endswith(_SENTINEL[1])))
|
|
597
|
+
if plain:
|
|
598
|
+
return value
|
|
599
|
+
return _SENTINEL[0] + base64.b64encode(value.encode("utf-8")).decode("ascii") + _SENTINEL[1]
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def decode_header_value(value: str) -> Optional[str]:
|
|
603
|
+
"""The body value a header carries, decoding the Base64 sentinel; None for a header value with
|
|
604
|
+
invalid characters or a sentinel that is not valid Base64 of UTF-8 (a server rejects both)."""
|
|
605
|
+
if value.startswith(_SENTINEL[0]) and value.endswith(_SENTINEL[1]) and len(value) >= len(_SENTINEL[0]) + len(_SENTINEL[1]):
|
|
606
|
+
try:
|
|
607
|
+
return base64.b64decode(value[len(_SENTINEL[0]):-len(_SENTINEL[1])], validate=True).decode("utf-8")
|
|
608
|
+
except ValueError: # binascii.Error and UnicodeDecodeError are both ValueErrors
|
|
609
|
+
return None
|
|
610
|
+
if any(not (0x20 <= ord(c) <= 0x7E or c == "\t") for c in value):
|
|
611
|
+
return None
|
|
612
|
+
return value
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def mcp_header_problem(message: Any, headers: Any) -> Optional[str]:
|
|
616
|
+
"""MCP 2026-07-28 Streamable HTTP mirrors the body in `Mcp-Method` (every request and
|
|
617
|
+
notification) and `Mcp-Name` (`params.name` or `params.uri` of tools/call, prompts/get,
|
|
618
|
+
resources/read). A server MUST reject a mismatch; a request that declares 2026-07-28 must
|
|
619
|
+
carry them. Earlier revisions may omit them, but what they send must still agree.
|
|
620
|
+
`headers` is any case-insensitive mapping with `.get` (http.server's message, a dict)."""
|
|
621
|
+
if not isinstance(message, dict) or headers is None:
|
|
622
|
+
return None
|
|
623
|
+
def header(name: str) -> Optional[str]:
|
|
624
|
+
value = headers.get(name) if hasattr(headers, "get") else None
|
|
625
|
+
if value is None and isinstance(headers, dict):
|
|
626
|
+
value = next((v for k, v in headers.items() if k.lower() == name.lower()), None)
|
|
627
|
+
if isinstance(value, (list, tuple)):
|
|
628
|
+
value = value[0] if value else None
|
|
629
|
+
return None if value is None else str(value)
|
|
630
|
+
declared = header("MCP-Protocol-Version") == MCP_REVISION
|
|
631
|
+
method = message.get("method")
|
|
632
|
+
sent = header("Mcp-Method")
|
|
633
|
+
if sent is None and declared:
|
|
634
|
+
return "the body names method %s but the required Mcp-Method header is absent" % method
|
|
635
|
+
if sent is not None and sent != method:
|
|
636
|
+
return "Mcp-Method %s disagrees with the body's method %s" % (sent, method)
|
|
637
|
+
field = NAMED_METHODS.get(str(method))
|
|
638
|
+
if field:
|
|
639
|
+
params = message.get("params")
|
|
640
|
+
value = params.get(field) if isinstance(params, dict) else None
|
|
641
|
+
named = header("Mcp-Name")
|
|
642
|
+
if named is None and declared:
|
|
643
|
+
return "the body carries params.%s but the required Mcp-Name header is absent" % field
|
|
644
|
+
if named is not None:
|
|
645
|
+
decoded = decode_header_value(named)
|
|
646
|
+
if decoded is None:
|
|
647
|
+
return "Mcp-Name is malformed: invalid characters or a broken =?base64?…?= value"
|
|
648
|
+
if decoded != str(value):
|
|
649
|
+
return "Mcp-Name disagrees with the body's params.%s" % field
|
|
650
|
+
return None
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def mcp_request_headers(method: str, params: Dict[str, Any]) -> Dict[str, str]:
|
|
654
|
+
"""The standard headers a 2026-07-28 client sends with one request."""
|
|
655
|
+
out = {"MCP-Protocol-Version": MCP_REVISION, "Mcp-Method": method}
|
|
656
|
+
field = NAMED_METHODS.get(method)
|
|
657
|
+
if field and params.get(field) is not None:
|
|
658
|
+
out["Mcp-Name"] = encode_header_value(str(params[field]))
|
|
659
|
+
return out
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""JavaScript's view of JSON values, once for the whole kit (FAA-65, layer L0).
|
|
2
3
|
|
|
3
4
|
The contract's answers are computed by TypeScript, so a kit that reads JSON the way Python does disagrees with them on
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""The call queue and the load report of DEC-0104 (contract `src/agent-network.ts`, FAC-SEM-131 to FAC-SEM-133), as the
|
|
2
3
|
contract's reference answers them: a node that hosts a job-backed agent queues its jobs per (agentRef, context) with
|
|
3
4
|
these and reports its load as `service-load/1` under `extensions/load/0.1`. Part of the service kit (FAA-289, layer L3);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""One copy of a service, locked before any side effect (LC-01, DEC-0057): the instance lock, the supervisor marker
|
|
2
3
|
in both its forms, and the wait budget under a supervisor. Part of the service kit (FAA-65, layer L3).
|
|
3
4
|
|
|
@@ -206,15 +207,24 @@ class InstanceLock:
|
|
|
206
207
|
return self
|
|
207
208
|
|
|
208
209
|
def release(self) -> None:
|
|
210
|
+
"""Let the lock go. Never raises, so a release in a `finally` never replaces the caller's own outcome (FAA-196
|
|
211
|
+
(11)): the descriptor number and the name are forgotten before they are closed, so a retry never closes a
|
|
212
|
+
number reused since; a close that fails (a deferred write error) still drops the name, as acquire does."""
|
|
209
213
|
if self._watch is not None:
|
|
210
214
|
self._watch.set()
|
|
211
215
|
self._watch = None
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
fd, name, self._fd, self._name = self._fd, self._name, None, None
|
|
217
|
+
try:
|
|
218
|
+
if fd is not None:
|
|
219
|
+
os.close(fd)
|
|
220
|
+
except OSError: # the descriptor is gone either way; the kernel frees the lock with it
|
|
221
|
+
pass
|
|
222
|
+
finally:
|
|
223
|
+
if name is not None:
|
|
224
|
+
try:
|
|
225
|
+
_drop_lock_name(name)
|
|
226
|
+
except OSError: # nothing to add to the caller's own outcome
|
|
227
|
+
pass
|
|
218
228
|
|
|
219
229
|
|
|
220
230
|
def lost_lock_message(lock_path: Path) -> str:
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_mandate.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""The operator's mandate, `mandate/1` (DEC-0125, FAC-SEM-144): the operator's instruction for one task travels with
|
|
2
3
|
the task, narrowed at each hand-over. Part of the service kit (layer L2); the twin of fabric-mandate.mjs.
|
|
3
4
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""OpenRouter chat completions for a service: its own key, a spend ceiling checked before every attempt, a usage
|
|
2
3
|
receipt for every answered call, and a fallback model of another vendor.
|
|
3
4
|
|
|
@@ -72,6 +73,45 @@ def vendor(model: str) -> str:
|
|
|
72
73
|
return str(model).split("/", 1)[0] if "/" in str(model) else ""
|
|
73
74
|
|
|
74
75
|
|
|
76
|
+
#: A base URL's path as both twins send it unchanged: URL path characters and %XX escapes only (FAA-187 (18)).
|
|
77
|
+
_BASE_PATH = re.compile(r"(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})*)*", re.ASCII)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _dialed_host(base: str) -> str:
|
|
81
|
+
"""The base with a host outside ASCII written as urllib dials it, in IDNA (lower case), so attribution reads the
|
|
82
|
+
host the call reaches, as the Node twin's `new URL` does (FAA-187 (19)): `openrouter.ai` behind a zero-width space
|
|
83
|
+
or a soft hyphen is openrouter.ai. check_url has already refused a host the codec cannot write."""
|
|
84
|
+
scheme, sep, rest = base.partition("://")
|
|
85
|
+
authority, slash, path = rest.partition("/")
|
|
86
|
+
userinfo, at, hostport = authority.rpartition("@")
|
|
87
|
+
if not sep or hostport.isascii() or hostport.startswith("["):
|
|
88
|
+
return base
|
|
89
|
+
host, colon, port = hostport.partition(":")
|
|
90
|
+
return "%s://%s%s%s%s%s%s" % (scheme, userinfo, at, host.encode("idna").decode("ascii").lower(), colon, port, slash + path)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def base_url_problem(base: str) -> Optional[str]:
|
|
94
|
+
"""Why a base URL is not sent the same by both twins, read from the URL as sent (its ends trimmed, as `new URL` and
|
|
95
|
+
urllib trim them), or None: a backslash anywhere (`new URL` reads it as a slash), and after a `//` authority a
|
|
96
|
+
path holding anything but URL path characters and %XX escapes (Python refused a space or a character outside
|
|
97
|
+
ASCII, Node sent it percent-encoded; Node removes a tab or a line break) or a dot segment, escaped or not (Node
|
|
98
|
+
collapses it). The Node twin's `baseUrlProblem` is the same rule (FAA-187 (18), kit-openrouter-cases.json)."""
|
|
99
|
+
text = base.strip("".join(map(chr, range(0x21))))
|
|
100
|
+
if "\\" in text:
|
|
101
|
+
return "the base URL holds a backslash"
|
|
102
|
+
if any(c in text for c in "\t\n\r"): # Node's URL parser removes them anywhere and sends the rest
|
|
103
|
+
return "the base URL holds a tab or a line break"
|
|
104
|
+
rest = text.partition(":")[2]
|
|
105
|
+
if not rest.startswith("//"):
|
|
106
|
+
return None # no authority as written: check_url reads it
|
|
107
|
+
path = "/" + rest[2:].partition("/")[2] if "/" in rest[2:] else ""
|
|
108
|
+
if not _BASE_PATH.fullmatch(path):
|
|
109
|
+
return "the base URL's path holds a character outside URL path characters and %XX escapes"
|
|
110
|
+
if any(re.sub("%2[eE]", ".", seg) in (".", "..") for seg in path.split("/")[1:]):
|
|
111
|
+
return "the base URL's path holds a dot segment"
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
75
115
|
_DECIMAL = re.compile(r"(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][-+]?[0-9]+)?", re.ASCII)
|
|
76
116
|
|
|
77
117
|
|
|
@@ -170,6 +210,9 @@ class Client:
|
|
|
170
210
|
raise fabric_http.HttpError("the URL does not parse", url="<unparseable url>", kind="refused")
|
|
171
211
|
if "?" in base_url or "#" in base_url:
|
|
172
212
|
raise OpenRouterError("the base URL holds no query or fragment", kind="config")
|
|
213
|
+
problem = base_url_problem(base_url)
|
|
214
|
+
if problem:
|
|
215
|
+
raise OpenRouterError(problem + " (both kits would not send it alike)", kind="config")
|
|
173
216
|
fabric_http.check_url(base_url)
|
|
174
217
|
_check_attribution(project_id, None)
|
|
175
218
|
self.ceiling_usd = float(ceiling_usd)
|
|
@@ -179,7 +222,7 @@ class Client:
|
|
|
179
222
|
self.window = window
|
|
180
223
|
# The base URL as check_url read it and urllib would send it: its ends trimmed, so `.../v1\n` is not sent
|
|
181
224
|
# as `.../v1\n/chat/completions` (0.56.0 seam r18; the Node twin trims the ends `new URL` does, product r20).
|
|
182
|
-
self.base_url = base_url.strip(fabric_http._URL_ENDS).rstrip("/")
|
|
225
|
+
self.base_url = _dialed_host(base_url.strip(fabric_http._URL_ENDS).rstrip("/"))
|
|
183
226
|
self.attribution = attribution
|
|
184
227
|
self.project_id = project_id
|
|
185
228
|
self.unmeasured_call_usd = unmeasured_call_usd
|