@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_a2a_server.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
2
|
+
"""The serving side and a bounded client of A2A 1.0 (FAA-10, DEC-0122): an Agent Card built from the service's
|
|
3
|
+
capabilities (`agent_card`, every skill an A2A `AgentSkill`, checked by `fabric_a2a.agent_card_findings` before it is
|
|
4
|
+
served), the JSON-RPC binding (`A2ABinding`: `SendMessage`, `SendStreamingMessage`, `GetTask`, `CancelTask`) mapped
|
|
5
|
+
onto the interop job store (`fabric_interop.JobStore`: a task is a job, a choice stop is `TASK_STATE_INPUT_REQUIRED`,
|
|
6
|
+
the result envelope an artifact), and `A2AClient` (`discover`, `call`, `get`, `cancel`) that reaches a peer only on an
|
|
7
|
+
origin the operator allowed, with its credential read from the environment `use_secret.py run` fills, never from a
|
|
8
|
+
file. The interface never takes the descriptor's token (DEC-0122 item 3): the card names its own scheme and the
|
|
9
|
+
service's `authorize` decides the caller. Part of the service kit (layer L4). Standard library only."""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import secrets
|
|
16
|
+
import time
|
|
17
|
+
from typing import Any, Callable, Dict, Iterator, List, Mapping, Optional, Sequence, Tuple
|
|
18
|
+
|
|
19
|
+
import sys as _sys # noqa: E402
|
|
20
|
+
from pathlib import Path as _Path # noqa: E402
|
|
21
|
+
if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from any working directory (FAA-65)
|
|
22
|
+
_sys.path.insert(0, str(_Path(__file__).resolve().parent))
|
|
23
|
+
|
|
24
|
+
from fabric_a2a import A2A_PROTOCOL_VERSION, A2A_SURFACE_PATH, agent_card_findings # noqa: E402
|
|
25
|
+
from fabric_base import ServiceError # noqa: E402
|
|
26
|
+
from fabric_interop import JobStore # noqa: E402
|
|
27
|
+
from fabric_interop_rules import InteropError, UnknownJob # noqa: E402
|
|
28
|
+
from fabric_http import request as http_request # noqa: E402
|
|
29
|
+
|
|
30
|
+
#: The JSON-RPC binding's name in a card's `supportedInterfaces` (A2A 1.0).
|
|
31
|
+
JSONRPC_BINDING = "JSONRPC"
|
|
32
|
+
#: The JSON-RPC codes A2A 1.0 section 5.4 maps its errors to, and JSON-RPC 2.0's own.
|
|
33
|
+
PARSE_ERROR, INVALID_REQUEST, METHOD_NOT_FOUND, INVALID_PARAMS, INTERNAL_ERROR = -32700, -32600, -32601, -32602, -32603
|
|
34
|
+
TASK_NOT_FOUND, TASK_NOT_CANCELABLE, UNSUPPORTED_OPERATION, CONTENT_TYPE_NOT_SUPPORTED, VERSION_NOT_SUPPORTED = \
|
|
35
|
+
-32001, -32002, -32004, -32005, -32009
|
|
36
|
+
#: The interop job status each A2A task state reads from.
|
|
37
|
+
TASK_STATES = {"working": "TASK_STATE_WORKING", "input_required": "TASK_STATE_INPUT_REQUIRED",
|
|
38
|
+
"completed": "TASK_STATE_COMPLETED", "failed": "TASK_STATE_FAILED", "cancelled": "TASK_STATE_CANCELED"}
|
|
39
|
+
TERMINAL_STATES = ("TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED", "TASK_STATE_REJECTED")
|
|
40
|
+
MAX_BODY = 1024 * 1024
|
|
41
|
+
_SKILL_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}\Z")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class A2AError(ServiceError):
|
|
45
|
+
"""A refused A2A call or card; the message never carries a credential."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def skill_from_capability(capability: Mapping[str, Any]) -> Dict[str, Any]:
|
|
49
|
+
"""An A2A `AgentSkill` from one capability ({name, description?, tags?, examples?, inputModes?, outputModes?}):
|
|
50
|
+
its id is the capability's name, the id the contract's A2A peer profile names in `skillIds`."""
|
|
51
|
+
name = capability.get("name") if isinstance(capability, Mapping) else None
|
|
52
|
+
if not isinstance(name, str) or not _SKILL_ID.match(name):
|
|
53
|
+
raise A2AError("a skill's id is the capability's name: letters, digits, '.', '_' or '-', at most 64")
|
|
54
|
+
skill: Dict[str, Any] = {"id": name, "name": capability.get("title") or name,
|
|
55
|
+
"description": capability.get("description") or name,
|
|
56
|
+
"tags": list(capability.get("tags") or [name.split(".")[0]])}
|
|
57
|
+
for key in ("examples", "inputModes", "outputModes"):
|
|
58
|
+
if capability.get(key):
|
|
59
|
+
skill[key] = list(capability[key])
|
|
60
|
+
return skill
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def agent_card(*, name: str, description: str, version: str, url: str, capabilities: Sequence[Mapping[str, Any]],
|
|
64
|
+
security_schemes: Optional[Dict[str, Any]] = None, streaming: bool = False,
|
|
65
|
+
extensions: Optional[List[Dict[str, Any]]] = None, remote: bool = False,
|
|
66
|
+
descriptor_header: str = "Authorization") -> Dict[str, Any]:
|
|
67
|
+
"""The public Agent Card a service serves at `surfaces.a2a.path` (A2A 1.0): one JSON-RPC interface at `url` on the
|
|
68
|
+
service's origin, one skill per capability. Raises A2AError when `agent_card_findings` finds anything, so a card
|
|
69
|
+
that names a process member, a secret, the descriptor's token header or a required Fabric extension is never
|
|
70
|
+
served. `security_schemes` names the interface's own scheme (never the descriptor's token)."""
|
|
71
|
+
match = re.match(r"\A(https?://[^/?#]+)", url or "")
|
|
72
|
+
card: Dict[str, Any] = {
|
|
73
|
+
"name": name, "description": description, "version": version,
|
|
74
|
+
"supportedInterfaces": [{"url": url, "protocolBinding": JSONRPC_BINDING, "protocolVersion": A2A_PROTOCOL_VERSION}],
|
|
75
|
+
"capabilities": {"streaming": bool(streaming), "pushNotifications": False},
|
|
76
|
+
"defaultInputModes": ["text/plain", "application/json"], "defaultOutputModes": ["text/plain", "application/json"],
|
|
77
|
+
"skills": [skill_from_capability(c) for c in capabilities],
|
|
78
|
+
}
|
|
79
|
+
if extensions:
|
|
80
|
+
card["capabilities"]["extensions"] = [dict(e, required=False) for e in extensions]
|
|
81
|
+
if security_schemes:
|
|
82
|
+
card["securitySchemes"] = security_schemes
|
|
83
|
+
findings = agent_card_findings(card, origin=match.group(1) if match else None, remote=remote,
|
|
84
|
+
descriptor_header=descriptor_header)
|
|
85
|
+
if findings:
|
|
86
|
+
raise A2AError("the card is not served: %s" % "; ".join(f["message"] for f in findings))
|
|
87
|
+
return card
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def text_of(message: Any) -> str:
|
|
91
|
+
"""The text parts of an A2A message, joined by a line feed."""
|
|
92
|
+
parts = message.get("parts") if isinstance(message, dict) else None
|
|
93
|
+
return "\n".join(p["text"] for p in parts or [] if isinstance(p, dict) and isinstance(p.get("text"), str))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def data_of(message: Any) -> List[Any]:
|
|
97
|
+
"""The data parts of an A2A message."""
|
|
98
|
+
parts = message.get("parts") if isinstance(message, dict) else None
|
|
99
|
+
return [p["data"] for p in parts or [] if isinstance(p, dict) and "data" in p]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def agent_message(text: str, *, task_id: Optional[str] = None, context_id: Optional[str] = None,
|
|
103
|
+
data: Any = None) -> Dict[str, Any]:
|
|
104
|
+
"""An agent's A2A message: a text part, and a data part when `data` is given."""
|
|
105
|
+
parts: List[Dict[str, Any]] = [{"text": text}]
|
|
106
|
+
if data is not None:
|
|
107
|
+
parts.append({"data": data})
|
|
108
|
+
message: Dict[str, Any] = {"role": "ROLE_AGENT", "parts": parts, "messageId": "msg-" + secrets.token_hex(8)}
|
|
109
|
+
if task_id:
|
|
110
|
+
message["taskId"] = task_id
|
|
111
|
+
if context_id:
|
|
112
|
+
message["contextId"] = context_id
|
|
113
|
+
return message
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TaskStart:
|
|
117
|
+
"""What a message handler gets: the message, its caller and `start(skill)`, which makes the job that is the task
|
|
118
|
+
and returns its id; the service then runs the work and moves the job (working, request_input, complete, fail)."""
|
|
119
|
+
|
|
120
|
+
def __init__(self, binding: "A2ABinding", message: Dict[str, Any], caller: Optional[str], context_id: str):
|
|
121
|
+
self.binding, self.message, self.caller, self.context_id = binding, message, caller, context_id
|
|
122
|
+
self.task_id: Optional[str] = None
|
|
123
|
+
|
|
124
|
+
def start(self, skill: str) -> str:
|
|
125
|
+
if skill not in self.binding.skills:
|
|
126
|
+
raise A2AError("no skill %s on this card" % skill)
|
|
127
|
+
handle = self.binding.jobs.create("a2a." + skill, {"a2a": {"message": self.message, "contextId": self.context_id,
|
|
128
|
+
"caller": self.caller}})
|
|
129
|
+
self.task_id = handle["job"]["id"]
|
|
130
|
+
return self.task_id
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
Handler = Callable[[TaskStart], Optional[Dict[str, Any]]]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class A2ABinding:
|
|
137
|
+
"""The JSON-RPC binding over the interop job store. `handler(start)` answers a new message: it returns an agent
|
|
138
|
+
message (`agent_message`) for a reply with no task, or calls `start.start(skill)` and returns None. `authorize`
|
|
139
|
+
turns the request's headers into a caller (or None to refuse with HTTP 401); a task is read, answered or cancelled
|
|
140
|
+
only by the caller that started it, any other gets TaskNotFound. `handle(body, headers)` returns (status, body)."""
|
|
141
|
+
|
|
142
|
+
def __init__(self, jobs: JobStore, handler: Handler, *, skills: Sequence[str],
|
|
143
|
+
authorize: Callable[[Mapping[str, str]], Optional[str]], clock: Callable[[], float] = time.time):
|
|
144
|
+
self.jobs, self.handler, self.skills, self.authorize, self.clock = jobs, handler, set(skills), authorize, clock
|
|
145
|
+
|
|
146
|
+
def task(self, job_id: str, caller: Optional[str]) -> Dict[str, Any]:
|
|
147
|
+
"""The A2A Task a job reads as; UnknownJob when it is not an A2A job of this caller."""
|
|
148
|
+
a2a = (self.jobs.private(job_id).get("input") or {}).get("a2a")
|
|
149
|
+
if not isinstance(a2a, dict) or a2a.get("caller") != caller:
|
|
150
|
+
raise UnknownJob(job_id)
|
|
151
|
+
return task_of(self.jobs.get(job_id), a2a.get("contextId"))
|
|
152
|
+
|
|
153
|
+
def handle(self, body: Any, headers: Mapping[str, str]) -> Tuple[int, Optional[Dict[str, Any]]]:
|
|
154
|
+
lowered = {str(k).lower(): v for k, v in (headers or {}).items()}
|
|
155
|
+
caller = self.authorize(lowered)
|
|
156
|
+
if caller is None:
|
|
157
|
+
return 401, None
|
|
158
|
+
if isinstance(body, (bytes, bytearray)):
|
|
159
|
+
if len(body) > MAX_BODY:
|
|
160
|
+
return 413, None
|
|
161
|
+
try:
|
|
162
|
+
body = json.loads(bytes(body).decode("utf-8"))
|
|
163
|
+
except ValueError:
|
|
164
|
+
return 200, _error(None, PARSE_ERROR, "the body is not JSON")
|
|
165
|
+
if not isinstance(body, dict) or body.get("jsonrpc") != "2.0" or not isinstance(body.get("method"), str):
|
|
166
|
+
return 200, _error(body.get("id") if isinstance(body, dict) else None, INVALID_REQUEST, "not a JSON-RPC 2.0 request")
|
|
167
|
+
rid = body.get("id")
|
|
168
|
+
version = lowered.get("a2a-version")
|
|
169
|
+
if version is not None and version.strip() != A2A_PROTOCOL_VERSION:
|
|
170
|
+
return 200, _error(rid, VERSION_NOT_SUPPORTED, "this agent speaks A2A %s" % A2A_PROTOCOL_VERSION)
|
|
171
|
+
params = body.get("params")
|
|
172
|
+
if not isinstance(params, dict):
|
|
173
|
+
return 200, _error(rid, INVALID_PARAMS, "params is an object")
|
|
174
|
+
method = body["method"]
|
|
175
|
+
try:
|
|
176
|
+
if method in ("SendMessage", "SendStreamingMessage"):
|
|
177
|
+
if method == "SendStreamingMessage":
|
|
178
|
+
return 200, _error(rid, UNSUPPORTED_OPERATION, "streaming is served by stream() as text/event-stream")
|
|
179
|
+
return 200, _result(rid, self.send(params, caller))
|
|
180
|
+
if method == "GetTask":
|
|
181
|
+
return 200, _result(rid, self.task(str(params.get("id")), caller))
|
|
182
|
+
if method == "CancelTask":
|
|
183
|
+
return 200, _result(rid, self.cancel(str(params.get("id")), caller))
|
|
184
|
+
except UnknownJob:
|
|
185
|
+
return 200, _error(rid, TASK_NOT_FOUND, "no such task")
|
|
186
|
+
except _NotCancelable:
|
|
187
|
+
return 200, _error(rid, TASK_NOT_CANCELABLE, "the task has ended")
|
|
188
|
+
except _BadParams as exc:
|
|
189
|
+
return 200, _error(rid, INVALID_PARAMS, str(exc))
|
|
190
|
+
except (A2AError, InteropError) as exc:
|
|
191
|
+
return 200, _error(rid, INTERNAL_ERROR, "the agent could not take the message: %s" % exc)
|
|
192
|
+
return 200, _error(rid, METHOD_NOT_FOUND, "no method %s here" % method[:64])
|
|
193
|
+
|
|
194
|
+
def send(self, params: Dict[str, Any], caller: Optional[str]) -> Dict[str, Any]:
|
|
195
|
+
message = params.get("message")
|
|
196
|
+
if not isinstance(message, dict) or not isinstance(message.get("parts"), list) or not message["parts"]:
|
|
197
|
+
raise _BadParams("params.message is a message with parts")
|
|
198
|
+
task_id = message.get("taskId")
|
|
199
|
+
if isinstance(task_id, str) and task_id:
|
|
200
|
+
return {"task": self._continue(task_id, message, caller)}
|
|
201
|
+
context_id = message.get("contextId") if isinstance(message.get("contextId"), str) else "ctx-" + secrets.token_hex(8)
|
|
202
|
+
start = TaskStart(self, message, caller, context_id)
|
|
203
|
+
reply = self.handler(start)
|
|
204
|
+
if start.task_id is not None:
|
|
205
|
+
return {"task": self.task(start.task_id, caller)}
|
|
206
|
+
if not isinstance(reply, dict):
|
|
207
|
+
raise A2AError("the handler neither replied nor started a task")
|
|
208
|
+
return {"message": dict(reply, contextId=context_id)}
|
|
209
|
+
|
|
210
|
+
def _continue(self, task_id: str, message: Dict[str, Any], caller: Optional[str]) -> Dict[str, Any]:
|
|
211
|
+
"""A message to an input-required task answers its question: a data part {inputResponses} as fabric.job.get
|
|
212
|
+
takes it, or plain text for a question with one field."""
|
|
213
|
+
task = self.task(task_id, caller)
|
|
214
|
+
if task["status"]["state"] != "TASK_STATE_INPUT_REQUIRED":
|
|
215
|
+
raise _BadParams("the task waits for no answer")
|
|
216
|
+
job = self.jobs.get(task_id)
|
|
217
|
+
responses = next((d["inputResponses"] for d in data_of(message) if isinstance(d, dict) and isinstance(d.get("inputResponses"), dict)), None)
|
|
218
|
+
if responses is None:
|
|
219
|
+
responses = _text_answer(job.get("inputRequests") or {}, text_of(message))
|
|
220
|
+
if not responses or not self.jobs.answer(task_id, responses):
|
|
221
|
+
raise _BadParams("the answer names none of the task's questions")
|
|
222
|
+
return self.task(task_id, caller)
|
|
223
|
+
|
|
224
|
+
def cancel(self, task_id: str, caller: Optional[str]) -> Dict[str, Any]:
|
|
225
|
+
task = self.task(task_id, caller)
|
|
226
|
+
if task["status"]["state"] in TERMINAL_STATES:
|
|
227
|
+
raise _NotCancelable()
|
|
228
|
+
self.jobs.cancel(task_id)
|
|
229
|
+
return self.task(task_id, caller)
|
|
230
|
+
|
|
231
|
+
def stream(self, params: Dict[str, Any], caller: Optional[str], *, rid: Any = None, poll_s: float = 1.0,
|
|
232
|
+
deadline_s: float = 300.0, sleep: Callable[[float], None] = time.sleep) -> Iterator[str]:
|
|
233
|
+
"""`SendStreamingMessage` as server-sent events: the task first, then a status update each time its state
|
|
234
|
+
changes, ending at a terminal or input-required state or the deadline. Each event is one `data:` line holding
|
|
235
|
+
a JSON-RPC response whose result is a StreamResponse."""
|
|
236
|
+
first = self.send(params, caller)
|
|
237
|
+
yield _event(rid, first)
|
|
238
|
+
if "task" not in first:
|
|
239
|
+
return
|
|
240
|
+
task, end = first["task"], self.clock() + deadline_s
|
|
241
|
+
while task["status"]["state"] not in TERMINAL_STATES + ("TASK_STATE_INPUT_REQUIRED",) and self.clock() < end:
|
|
242
|
+
sleep(poll_s)
|
|
243
|
+
now = self.task(task["id"], caller)
|
|
244
|
+
if now["status"]["state"] != task["status"]["state"] or now.get("artifacts") != task.get("artifacts"):
|
|
245
|
+
final = now["status"]["state"] in TERMINAL_STATES + ("TASK_STATE_INPUT_REQUIRED",)
|
|
246
|
+
yield _event(rid, {"statusUpdate": {"taskId": now["id"], "contextId": now["contextId"],
|
|
247
|
+
"status": now["status"], "final": final}})
|
|
248
|
+
task = now
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class _NotCancelable(Exception):
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class _BadParams(Exception):
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _text_answer(requests: Dict[str, Any], text: str) -> Optional[Dict[str, Any]]:
|
|
260
|
+
if len(requests) != 1 or not text:
|
|
261
|
+
return None
|
|
262
|
+
key, request = next(iter(requests.items()))
|
|
263
|
+
props = (((request.get("params") or {}).get("requestedSchema") or {}).get("properties") or {}) if isinstance(request, dict) else {}
|
|
264
|
+
if len(props) != 1:
|
|
265
|
+
return None
|
|
266
|
+
return {key: {"action": "accept", "content": {next(iter(props)): text}}}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def task_of(job: Dict[str, Any], context_id: Optional[str]) -> Dict[str, Any]:
|
|
270
|
+
"""The A2A Task an interop job reads as: its state, a status message, the result envelope as an artifact."""
|
|
271
|
+
state = TASK_STATES.get(job.get("status"), "TASK_STATE_UNSPECIFIED")
|
|
272
|
+
status: Dict[str, Any] = {"state": state, "timestamp": job.get("updatedAt") or ""}
|
|
273
|
+
text = job.get("statusMessage")
|
|
274
|
+
if job.get("status") == "input_required":
|
|
275
|
+
questions = [(r.get("params") or {}).get("message") for r in (job.get("inputRequests") or {}).values() if isinstance(r, dict)]
|
|
276
|
+
status["message"] = agent_message(text or "; ".join(q for q in questions if isinstance(q, str)) or "an answer is needed",
|
|
277
|
+
task_id=job["id"], context_id=context_id,
|
|
278
|
+
data={"inputRequests": job.get("inputRequests")})
|
|
279
|
+
elif job.get("status") == "failed":
|
|
280
|
+
error = job.get("error") or {}
|
|
281
|
+
status["message"] = agent_message(str(error.get("message") or "the task failed"), task_id=job["id"],
|
|
282
|
+
context_id=context_id, data={"code": error.get("code")})
|
|
283
|
+
elif text:
|
|
284
|
+
status["message"] = agent_message(text, task_id=job["id"], context_id=context_id)
|
|
285
|
+
task: Dict[str, Any] = {"id": job["id"], "contextId": context_id, "status": status}
|
|
286
|
+
if job.get("status") == "completed":
|
|
287
|
+
task["artifacts"] = [{"artifactId": "result", "name": "result", "parts": [{"data": job.get("result")}]}]
|
|
288
|
+
return task
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _result(rid: Any, result: Any) -> Dict[str, Any]:
|
|
292
|
+
return {"jsonrpc": "2.0", "id": rid, "result": result}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _error(rid: Any, code: int, message: str) -> Dict[str, Any]:
|
|
296
|
+
return {"jsonrpc": "2.0", "id": rid, "error": {"code": code, "message": message}}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _event(rid: Any, result: Any) -> str:
|
|
300
|
+
return "data: %s\n\n" % json.dumps(_result(rid, result), separators=(",", ":"), ensure_ascii=False)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def origin_of(url: str) -> str:
|
|
304
|
+
"""`scheme://host[:port]` of a URL, lower-cased, with a default port dropped; A2AError for anything else."""
|
|
305
|
+
match = re.match(r"\A(https?)://([^/?#]+)", url or "", re.IGNORECASE)
|
|
306
|
+
if not match or "@" in match.group(2):
|
|
307
|
+
raise A2AError("a peer's address is an http(s) origin with no userinfo")
|
|
308
|
+
scheme, host = match.group(1).lower(), match.group(2).lower()
|
|
309
|
+
if (scheme, host.rsplit(":", 1)[-1]) in (("https", "443"), ("http", "80")) and ":" in host and not host.endswith("]"):
|
|
310
|
+
host = host.rsplit(":", 1)[0]
|
|
311
|
+
return "%s://%s" % (scheme, host)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class A2AClient:
|
|
315
|
+
"""A bounded A2A 1.0 client: every call has a deadline and a size cap (fabric_http), reaches only an origin in
|
|
316
|
+
`allow_origins` (the operator's list), and sends a credential only when `credential_env` names an environment
|
|
317
|
+
variable that holds one (`use_secret.py run --vault-only <project> <NAME> -- <command>` fills it; never a file
|
|
318
|
+
beside the agent). It never follows a redirect and never retries a SendMessage."""
|
|
319
|
+
|
|
320
|
+
def __init__(self, allow_origins: Sequence[str], *, credential_env: Optional[str] = None,
|
|
321
|
+
header: str = "Authorization", scheme: Optional[str] = "Bearer", deadline_s: float = 30.0,
|
|
322
|
+
max_bytes: int = MAX_BODY, transport: Any = None, insecure: bool = False):
|
|
323
|
+
self.allowed = {origin_of(o) for o in allow_origins}
|
|
324
|
+
if not self.allowed:
|
|
325
|
+
raise A2AError("a client needs the operator's list of peer origins")
|
|
326
|
+
self.credential_env, self.header, self.scheme = credential_env, header, scheme
|
|
327
|
+
self.deadline_s, self.max_bytes, self.transport, self.insecure = deadline_s, max_bytes, transport, insecure
|
|
328
|
+
self._ids = 0
|
|
329
|
+
|
|
330
|
+
def _check(self, url: str) -> None:
|
|
331
|
+
if origin_of(url) not in self.allowed:
|
|
332
|
+
raise A2AError("%s is not on the operator's list of A2A peers" % origin_of(url))
|
|
333
|
+
|
|
334
|
+
def _headers(self) -> Dict[str, str]:
|
|
335
|
+
headers = {"A2A-Version": A2A_PROTOCOL_VERSION, "Accept": "application/json"}
|
|
336
|
+
if self.credential_env:
|
|
337
|
+
value = os.environ.get(self.credential_env)
|
|
338
|
+
if not value:
|
|
339
|
+
raise A2AError("%s is empty: run the caller under use_secret.py run, which places the key" % self.credential_env)
|
|
340
|
+
headers[self.header] = "%s %s" % (self.scheme, value) if self.scheme else value
|
|
341
|
+
return headers
|
|
342
|
+
|
|
343
|
+
def _get(self, url: str) -> Any:
|
|
344
|
+
self._check(url)
|
|
345
|
+
kwargs = {"transport": self.transport} if self.transport else {}
|
|
346
|
+
response = http_request("GET", url, headers={"Accept": "application/json"}, deadline_s=self.deadline_s,
|
|
347
|
+
max_bytes=self.max_bytes, insecure=self.insecure, **kwargs)
|
|
348
|
+
if response.status != 200:
|
|
349
|
+
raise A2AError("the peer answered HTTP %d for its card" % response.status)
|
|
350
|
+
try:
|
|
351
|
+
return response.json()
|
|
352
|
+
except ValueError:
|
|
353
|
+
raise A2AError("the peer's card is not JSON") from None
|
|
354
|
+
|
|
355
|
+
def discover(self, origin: str) -> Dict[str, Any]:
|
|
356
|
+
"""The peer's public card and the JSON-RPC interface to call: {card, url}."""
|
|
357
|
+
card = self._get(origin_of(origin) + A2A_SURFACE_PATH)
|
|
358
|
+
if not isinstance(card, dict):
|
|
359
|
+
raise A2AError("the peer's card is not a JSON object")
|
|
360
|
+
for interface in card.get("supportedInterfaces") or []:
|
|
361
|
+
if (isinstance(interface, dict) and interface.get("protocolBinding") == JSONRPC_BINDING
|
|
362
|
+
and interface.get("protocolVersion") == A2A_PROTOCOL_VERSION and isinstance(interface.get("url"), str)):
|
|
363
|
+
self._check(interface["url"])
|
|
364
|
+
return {"card": card, "url": interface["url"]}
|
|
365
|
+
raise A2AError("the peer's card names no A2A %s JSON-RPC interface" % A2A_PROTOCOL_VERSION)
|
|
366
|
+
|
|
367
|
+
def _rpc(self, url: str, method: str, params: Dict[str, Any]) -> Any:
|
|
368
|
+
self._check(url)
|
|
369
|
+
self._ids += 1
|
|
370
|
+
kwargs = {"transport": self.transport} if self.transport else {}
|
|
371
|
+
response = http_request("POST", url, headers=self._headers(), body={"jsonrpc": "2.0", "id": self._ids,
|
|
372
|
+
"method": method, "params": params}, deadline_s=self.deadline_s, retries=0,
|
|
373
|
+
max_bytes=self.max_bytes, insecure=self.insecure, **kwargs)
|
|
374
|
+
if response.status != 200:
|
|
375
|
+
raise A2AError("the peer answered HTTP %d to %s" % (response.status, method))
|
|
376
|
+
try:
|
|
377
|
+
answer = response.json()
|
|
378
|
+
except ValueError:
|
|
379
|
+
answer = None
|
|
380
|
+
if not isinstance(answer, dict):
|
|
381
|
+
raise A2AError("the peer's answer to %s is not a JSON-RPC response" % method)
|
|
382
|
+
if "error" in answer:
|
|
383
|
+
error = answer["error"] if isinstance(answer["error"], dict) else {}
|
|
384
|
+
raise A2AError("the peer refused %s: %s %s" % (method, error.get("code"), str(error.get("message"))[:200]))
|
|
385
|
+
return answer.get("result")
|
|
386
|
+
|
|
387
|
+
def call(self, url: str, text: str, *, data: Any = None, task_id: Optional[str] = None,
|
|
388
|
+
context_id: Optional[str] = None) -> Any:
|
|
389
|
+
"""SendMessage: a user message with `text` (and a data part), new or continuing `task_id`; the result as sent
|
|
390
|
+
({task} or {message})."""
|
|
391
|
+
parts: List[Dict[str, Any]] = [{"text": text}]
|
|
392
|
+
if data is not None:
|
|
393
|
+
parts.append({"data": data})
|
|
394
|
+
message: Dict[str, Any] = {"role": "ROLE_USER", "parts": parts, "messageId": "msg-" + secrets.token_hex(8)}
|
|
395
|
+
if task_id:
|
|
396
|
+
message["taskId"] = task_id
|
|
397
|
+
if context_id:
|
|
398
|
+
message["contextId"] = context_id
|
|
399
|
+
return self._rpc(url, "SendMessage", {"message": message})
|
|
400
|
+
|
|
401
|
+
def get(self, url: str, task_id: str) -> Any:
|
|
402
|
+
return self._rpc(url, "GetTask", {"id": task_id})
|
|
403
|
+
|
|
404
|
+
def cancel(self, url: str, task_id: str) -> Any:
|
|
405
|
+
return self._rpc(url, "CancelTask", {"id": task_id})
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""Every action a person has, its agent tool and who decides it: `fabric-actions/0.1`, the extension keyed
|
|
2
3
|
extensions/actions/0.1 (DEC-0121, FAC-SEM-142). Part of the service kit (layer L3); the twin of fabric-actions.mjs.
|
|
3
4
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""Who may call a service: the descriptor token, the network guard,
|
|
2
3
|
the operator's one-time login and its session, remote placement (DEC-0019) and the feed client's token header.
|
|
3
4
|
Part of the service kit (FAA-65, layer L3)."""
|
|
@@ -40,6 +41,7 @@ from fabric_descriptor import (
|
|
|
40
41
|
names_repository,
|
|
41
42
|
source_refusal,
|
|
42
43
|
validate_descriptor,
|
|
44
|
+
warn_no_source,
|
|
43
45
|
write_descriptor,
|
|
44
46
|
)
|
|
45
47
|
|
|
@@ -399,7 +401,7 @@ def register_remote(*, service_id: str, name: str, origin: str, token: str, inst
|
|
|
399
401
|
if source is None: # a rotation that names no source keeps the installed one (0.62.0 seam reading 1)
|
|
400
402
|
try:
|
|
401
403
|
installed = json.loads(descriptor_path(service_id, instance, root).read_text(encoding="utf-8")).get("source")
|
|
402
|
-
except (OSError, ValueError, AttributeError):
|
|
404
|
+
except (OSError, ValueError, AttributeError, RecursionError): # nested thousands deep (FAA-200 (10))
|
|
403
405
|
installed = None
|
|
404
406
|
# Only one the kit accepts: a hand-edited source never blocks a rotation; it is dropped with the warning.
|
|
405
407
|
base = set(validate_descriptor(descriptor))
|
|
@@ -415,9 +417,12 @@ def register_remote(*, service_id: str, name: str, origin: str, token: str, inst
|
|
|
415
417
|
refusal = source_refusal(descriptor, root) # before the token is written: a refused install leaves nothing (FAA-198)
|
|
416
418
|
if refusal:
|
|
417
419
|
raise ServiceError(refusal)
|
|
420
|
+
warned = not names_repository(descriptor)
|
|
421
|
+
if warned: # before the token: under warnings as errors neither file changes (FAA-200 (1))
|
|
422
|
+
warn_no_source(descriptor, 2) # the warning names the installer's call
|
|
418
423
|
ensure_private_dir(token_file.parent)
|
|
419
424
|
atomic_write(token_file, token.strip().encode(), 0o600)
|
|
420
|
-
return write_descriptor(descriptor, root,
|
|
425
|
+
return write_descriptor(descriptor, root, _warned=warned)
|
|
421
426
|
|
|
422
427
|
|
|
423
428
|
def cookie_value(cookie_header: Optional[str], name: str = SESSION_COOKIE) -> Optional[str]:
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""Settings backups (DEC-0025, FAC-SEM-035): the canonical form, the digest, writing, listing, checking and
|
|
2
3
|
restoring them. Part of the service kit (FAA-65, layer L3)."""
|
|
3
4
|
from __future__ import annotations
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""What every module of the service kit shares (FAA-65, layer L1): the protocol name, ServiceError, the
|
|
2
3
|
platform, ids and now_iso. Standard library only."""
|
|
3
4
|
from __future__ import annotations
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""The project board connector, Python: the facade (fabric-project-comms/0.1).
|
|
2
3
|
|
|
3
4
|
Since 0.40.0 (FAA-65) the connector is layered modules, lowest first: fabric_board_shapes (the wire), fabric_board_link
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""A fixture conversation for the probe (check_service.py, rule board.responder): a scripted board that a responder
|
|
2
3
|
can be run against without Fabric. Part of the board connector (FAA-65, layer L4)."""
|
|
3
4
|
from __future__ import annotations
|
|
@@ -13,6 +14,7 @@ from pathlib import Path as _Path # noqa: E402
|
|
|
13
14
|
if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from any working directory (FAA-65)
|
|
14
15
|
_sys.path.insert(0, str(_Path(__file__).resolve().parent))
|
|
15
16
|
import fabric_hub as _hub
|
|
17
|
+
from fabric_events import LEVELS as _LEVELS # noqa: E402 - the levels the event log takes (FAA-187 (23))
|
|
16
18
|
from fabric_board_shapes import ( # noqa: E402 - the kit (FAA-65)
|
|
17
19
|
COMMS_PROTOCOL,
|
|
18
20
|
RESPOND_TOOLS,
|
|
@@ -55,7 +57,9 @@ def _fixture_answers(variant: str) -> Dict[str, Any]:
|
|
|
55
57
|
def self_check() -> List[str]:
|
|
56
58
|
"""One scripted conversation per path through this kit, with no Fabric: the whole conversation, a `fenced` accept
|
|
57
59
|
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.
|
|
60
|
+
conversation as the contract asks. Writes only in a temporary folder it removes. The responder's notices are recorded
|
|
61
|
+
by an emit that takes a subject, as JsonlEventLog.append does: a notice at a level the log refuses, one without its
|
|
62
|
+
subject, or a fenced attempt that says no warning fails it (FAA-187 (23))."""
|
|
59
63
|
import os
|
|
60
64
|
import tempfile
|
|
61
65
|
problems: List[str] = []
|
|
@@ -71,7 +75,10 @@ def self_check() -> List[str]:
|
|
|
71
75
|
cred = Path(tmp) / "credential"
|
|
72
76
|
cred.write_text("c" * 43)
|
|
73
77
|
os.chmod(cred, 0o600)
|
|
74
|
-
answers, calls, handled = _fixture_answers(variant), [], []
|
|
78
|
+
answers, calls, handled, notices = _fixture_answers(variant), [], [], []
|
|
79
|
+
|
|
80
|
+
def emit(kind, level, text, subject=None, **extra):
|
|
81
|
+
notices.append((kind, level, subject))
|
|
75
82
|
|
|
76
83
|
def post(origin, body, bearer, session=None, timeout=15.0, method="POST", protocol_version=None):
|
|
77
84
|
if method == "DELETE":
|
|
@@ -93,9 +100,16 @@ def self_check() -> List[str]:
|
|
|
93
100
|
credential_file=cred, checkpoint_file=Path(tmp) / "checkpoint.json",
|
|
94
101
|
handler=lambda request, ctx: handled.append(request.body) or {"state": "completed", "summary": "Rendered."},
|
|
95
102
|
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))
|
|
103
|
+
session_factory=lambda grant: BoardSession(grant, post=post), emit=emit)
|
|
97
104
|
responder.step()
|
|
98
105
|
tools = [t for t, _ in calls]
|
|
106
|
+
for kind, level, subject in notices:
|
|
107
|
+
if level not in _LEVELS:
|
|
108
|
+
problems.append("%s: notice %s at level %r, which the event log refuses" % (variant, kind, level))
|
|
109
|
+
if not (isinstance(subject, dict) and subject.get("type") and subject.get("id")):
|
|
110
|
+
problems.append("%s: notice %s without its subject" % (variant, kind))
|
|
111
|
+
if variant == "fenced" and not any(level == "warning" for _, level, _ in notices):
|
|
112
|
+
problems.append("fenced: no warning notice reached the service's emit")
|
|
99
113
|
if any(t not in RESPOND_TOOLS and t != "fabric.board.join" for t in tools):
|
|
100
114
|
problems.append("%s: a tool outside the respond scope was sent" % variant)
|
|
101
115
|
if variant == "whole":
|