@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_board_link.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""The board link (DEC-0078, FAA-12b): the board session over the hub, the fsynced checkpoint and a claimed request as
|
|
2
3
|
data. Part of the board connector (FAA-65, layer L4), below the responder in fabric_board_session, which re-exports these
|
|
3
4
|
names. Standard library only, Python 3.9+."""
|
|
4
5
|
from __future__ import annotations
|
|
5
6
|
|
|
7
|
+
import inspect
|
|
6
8
|
import json
|
|
7
9
|
import math
|
|
8
10
|
import re
|
|
@@ -98,17 +100,29 @@ class BoardSession:
|
|
|
98
100
|
return value
|
|
99
101
|
|
|
100
102
|
def close(self, timeout: Optional[float] = None) -> bool:
|
|
101
|
-
"""Sends the DELETE (waiting `timeout` seconds at most when above 0,
|
|
103
|
+
"""Sends the DELETE (waiting `timeout` seconds at most for the whole of it when above 0, FAA-187 (12), else the
|
|
104
|
+
session's per read); True when Fabric answered it with a
|
|
102
105
|
2xx, so the slots are free now. False otherwise, a 404 (the session was already gone) included: a move made
|
|
103
106
|
before it ended may be read only at the next com.status, so the slot records stay; the lease lapses in 60 s."""
|
|
104
107
|
with self._lock:
|
|
105
108
|
if self.mcp_session is None:
|
|
106
109
|
return False
|
|
110
|
+
got: List[Any] = []
|
|
111
|
+
|
|
112
|
+
def send() -> None:
|
|
113
|
+
try:
|
|
114
|
+
got.append(self._send({}, method="DELETE", timeout=timeout))
|
|
115
|
+
except Exception: # noqa: BLE001 - any failure is no release, as the Node twin's catch reads it
|
|
116
|
+
pass
|
|
107
117
|
try:
|
|
108
|
-
|
|
118
|
+
if timeout is not None and timeout > 0: # the whole DELETE, not each socket read (FAA-187 (12))
|
|
119
|
+
worker = threading.Thread(target=send, name="board-leave", daemon=True)
|
|
120
|
+
worker.start()
|
|
121
|
+
worker.join(timeout)
|
|
122
|
+
else:
|
|
123
|
+
send()
|
|
124
|
+
answer = got[0] if got else None
|
|
109
125
|
status = answer[0] if isinstance(answer, (tuple, list)) and answer and isinstance(answer[0], int) else 0
|
|
110
|
-
except Exception: # noqa: BLE001 - any failure is no release, as the Node twin's catch reads it
|
|
111
|
-
status = 0
|
|
112
126
|
finally:
|
|
113
127
|
self.mcp_session = None # an answer it cannot read is no release, as the Node twin reads it
|
|
114
128
|
return 200 <= status < 300
|
|
@@ -263,6 +277,10 @@ def _bounded_int(literal: str) -> int:
|
|
|
263
277
|
return int(literal)
|
|
264
278
|
|
|
265
279
|
|
|
280
|
+
#: `mandate/1` as a block may write it: its slash plain or JSON-escaped (`\\/`, `\\u002f`).
|
|
281
|
+
_KIND_WRITTEN = re.compile(r"mandate(?:/|\\/|\\u002[fF])1")
|
|
282
|
+
|
|
283
|
+
|
|
266
284
|
def read_mandate(request: "BoardRequest", mandate_from: Any, agent_id: Optional[str]) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
267
285
|
"""The mandate a request carries, read only when the service opted in (`agent_id` and `mandate_from` set), the board
|
|
268
286
|
verified the sender (`provenance: trusted`) and its Project is one `mandate_from` names (or `PERSONAL_BOARD`): a T1
|
|
@@ -285,7 +303,7 @@ def read_mandate(request: "BoardRequest", mandate_from: Any, agent_id: Optional[
|
|
|
285
303
|
try:
|
|
286
304
|
value = json.loads(block, parse_constant=_no_constant, parse_int=_bounded_int)
|
|
287
305
|
except (ValueError, RecursionError):
|
|
288
|
-
if
|
|
306
|
+
if _KIND_WRITTEN.search(block): # the kind as written, a JSON escape of its slash included (FAA-196 (13))
|
|
289
307
|
return None, "mandate-unreadable"
|
|
290
308
|
continue
|
|
291
309
|
if isinstance(value, dict) and value.get("kind") == "mandate/1":
|
|
@@ -300,9 +318,10 @@ CALL_MEMBERS = ("capability", "action", "effect", "spendUsd", "project") # what
|
|
|
300
318
|
|
|
301
319
|
|
|
302
320
|
def covers(mandate: Optional[Dict[str, Any]], call: Any, agent_id: Optional[str], wall: Callable[[], float],
|
|
303
|
-
caller: Any = None) -> Optional[Dict[str, Any]]:
|
|
304
|
-
"""`mandate_covers` for a call under a board request's mandate, its sender placed as T1
|
|
305
|
-
|
|
321
|
+
caller: Any = None, caller_agent: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
|
322
|
+
"""`mandate_covers` for a call under a board request's mandate, its sender placed as T1. The caller's agent id is
|
|
323
|
+
`caller_agent`, what the service's identify_agent hook named for the sender (`sender_agent`, FAA-135); without one
|
|
324
|
+
the chain's end alone binds it (the board names a Project's principal, MD-3). A covered
|
|
306
325
|
answer's `recordAs` names `caller`, the sender Project the board verified (MD-3 records the caller, as the action
|
|
307
326
|
path does). None when no mandate travelled: the handler asks the operator as it would without one. The call's
|
|
308
327
|
`spendUsd` and `project` are read failing closed, as the kit's other mandate readers read them (FAA-139): a spend
|
|
@@ -334,12 +353,54 @@ def covers(mandate: Optional[Dict[str, Any]], call: Any, agent_id: Optional[str]
|
|
|
334
353
|
now = _format_rfc3339(clock * 1000) if math.isfinite(clock) else ""
|
|
335
354
|
except (OverflowError, ValueError): # finite but outside the dates a clock can name
|
|
336
355
|
now = ""
|
|
337
|
-
|
|
356
|
+
inputs = {"mandate": mandate, "tier": "T1", "now": now, "self": agent_id, "call": call}
|
|
357
|
+
if caller_agent is not None:
|
|
358
|
+
inputs["callerAgent"] = caller_agent
|
|
359
|
+
answer = _mandate.mandate_covers(inputs)
|
|
338
360
|
if "recordAs" in answer:
|
|
339
361
|
answer = {"ok": True, "recordAs": dict(answer["recordAs"], caller=caller)}
|
|
340
362
|
return answer
|
|
341
363
|
|
|
342
364
|
|
|
365
|
+
def notice_links(links: Any) -> Dict[str, str]:
|
|
366
|
+
"""A responder's `links` (FAA-187 (21)): the page on the service that resolves each notify kind, such as
|
|
367
|
+
`{"board.slot.moved": "/#board"}`, passed to a notice that notifies as its `link` (events-and-notifications.md).
|
|
368
|
+
A path on the service, as make_event takes one; None is none. A service that gives links has an emit that takes
|
|
369
|
+
`link` (JsonlEventLog.append does)."""
|
|
370
|
+
if links is None:
|
|
371
|
+
return {}
|
|
372
|
+
if not isinstance(links, Mapping) or not all(isinstance(k, str) and isinstance(v, str) and v.startswith("/")
|
|
373
|
+
and not v.startswith("//") for k, v in links.items()):
|
|
374
|
+
raise BoardError("invalid_arguments", "links maps an event kind to a path on this service, such as /#board.")
|
|
375
|
+
return dict(links)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def read_back_problem(message: Any, entry: Dict[str, Any], project: str) -> Optional[str]:
|
|
379
|
+
"""Why a message read back with com.get is not the request a claim took (the first path refuses it, a resume reads
|
|
380
|
+
no mandate from it, FAA-196 (1)): `digest-mismatch` (another digest than the claim's), `not-addressed` (not this
|
|
381
|
+
request, capability and Project), else None."""
|
|
382
|
+
if not isinstance(message, dict) or message.get("digest") != entry["digest"]:
|
|
383
|
+
return "digest-mismatch"
|
|
384
|
+
req = message.get("request")
|
|
385
|
+
if not isinstance(req, dict) or req.get("id") != entry["request"] or req.get("capability") != entry["capability"] \
|
|
386
|
+
or req.get("target") != project:
|
|
387
|
+
return "not-addressed"
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def sender_agent(identify_agent: Optional[Callable[[Any], Optional[str]]], sender: Any, mandate: Any = True) -> Optional[str]:
|
|
392
|
+
"""FAA-135: the agent id the service's identify_agent(sender) names for a request's sender (the board's verified
|
|
393
|
+
`{project, principal}`), read as the interop kit reads its own hook (`identified_agent`): None is none, an agent id
|
|
394
|
+
is that id, anything else, or a hook that raises, is an id no chain holds, so the mandate is refused. Asked only
|
|
395
|
+
when a mandate travelled (`mandate` not None)."""
|
|
396
|
+
if identify_agent is None or mandate is None:
|
|
397
|
+
return None
|
|
398
|
+
try:
|
|
399
|
+
return _rules.identified_agent(identify_agent(sender))
|
|
400
|
+
except Exception: # noqa: BLE001 - the service's own hook: fail closed
|
|
401
|
+
return _rules.UNREADABLE_AGENT
|
|
402
|
+
|
|
403
|
+
|
|
343
404
|
def mandate_options(agent_id: Any, mandate_from: Any) -> Any:
|
|
344
405
|
"""The responder's opt-in, checked: an agent id the registry accepts, and `mandate_from` either `PERSONAL_BOARD` or
|
|
345
406
|
a collection of Project ids (strings). Answers `mandate_from` as the responder keeps it (a frozenset or the string)."""
|
|
@@ -359,3 +420,31 @@ def mandate_options(agent_id: Any, mandate_from: Any) -> Any:
|
|
|
359
420
|
def _fold(project: str) -> str:
|
|
360
421
|
"""A Project id as Fabric's board compares it: a uuid lower-cased, any other id as written (_hub._project)."""
|
|
361
422
|
return project.lower() if _hub.UUID.fullmatch(project) else project
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def notice_subject(value: Dict[str, Any], project: str, episode: Optional[str] = None) -> Dict[str, str]:
|
|
426
|
+
"""A notice's subject (`{type, id}`): its request, else its capability, else its episode (a cause such as a block's
|
|
427
|
+
code, so two causes are two episodes for a host, seam r31), else the Project."""
|
|
428
|
+
for kind, given in (("request", value.get("request")), ("capability", value.get("capability")), ("episode", episode)):
|
|
429
|
+
if isinstance(given, str) and 0 < len(given) <= 128:
|
|
430
|
+
return {"type": kind, "id": given}
|
|
431
|
+
return {"type": "project", "id": project}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def takes_subject(emit: Callable[..., Any]) -> bool:
|
|
435
|
+
"""Whether an emit's signature names a `subject` parameter, as JsonlEventLog.append does. Any other emit (one with
|
|
436
|
+
only **kwargs, which may forward them to a sink without it or set its own, or one whose signature cannot be read)
|
|
437
|
+
is called as 0.55.0 called it, without one (0.56.0 unit r33). A subject already bound into the emit, as with
|
|
438
|
+
functools.partial(log.append, subject=...), stands.
|
|
439
|
+
|
|
440
|
+
The signature is the emit's own, not a wrapped function's: a functools.wraps forwarder that sets its own subject
|
|
441
|
+
would otherwise be passed a second one (0.56.0 unit r34). Nothing here raises, so a notice never stops the
|
|
442
|
+
responder. A positional-only `subject` cannot take the keyword the responder passes, so it counts as none (FAA-187
|
|
443
|
+
(26)); the responder's own module imports both helpers under their old private names (moved here in 0.83.0).
|
|
444
|
+
"""
|
|
445
|
+
try:
|
|
446
|
+
params = inspect.signature(emit, follow_wrapped=False).parameters.values()
|
|
447
|
+
return any(p.name == "subject" and p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
|
|
448
|
+
and (p.default is inspect.Parameter.empty or p.default is None) for p in params)
|
|
449
|
+
except Exception: # noqa: BLE001 - an unreadable signature (or a default that raises) is called without one
|
|
450
|
+
return False
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""The board responder (DEC-0078, FAA-12b): the work context a handler sees and the responder loop (enroll, claim,
|
|
2
3
|
accept the exact digest, effect, progress, complete; stop when fenced). The session, the checkpoint and the request live
|
|
3
4
|
in fabric_board_link and are re-exported here. Part of the board connector (FAA-65, layer L4)."""
|
|
4
5
|
from __future__ import annotations
|
|
5
6
|
|
|
6
|
-
import inspect
|
|
7
7
|
import json
|
|
8
8
|
import random
|
|
9
9
|
import re
|
|
@@ -28,6 +28,8 @@ from fabric_board_link import ( # noqa: E402,F401 - re-exported: callers import
|
|
|
28
28
|
BoardSession,
|
|
29
29
|
Checkpoint,
|
|
30
30
|
PERSONAL_BOARD,
|
|
31
|
+
notice_subject as _notice_subject,
|
|
32
|
+
takes_subject as _takes_subject,
|
|
31
33
|
)
|
|
32
34
|
from fabric_board_slots import ( # noqa: E402,F401 - the slot records (FAA-155, FAA-163); the constants re-exported
|
|
33
35
|
HOLD_RETRY_MAX_SECONDS,
|
|
@@ -51,6 +53,9 @@ IDLE_LEAVE_SECONDS = 3.0
|
|
|
51
53
|
|
|
52
54
|
EVENT_SCHEMA = "urn:fabric:schema:board-responder-event:1"
|
|
53
55
|
|
|
56
|
+
#: `board.down` is said once per outage window: said again only after the board stayed up this long (FAA-187 (22)).
|
|
57
|
+
DOWN_QUIET_SECONDS = 300.0
|
|
58
|
+
|
|
54
59
|
#: On these the attempt is no longer ours: stop acting at once (C4).
|
|
55
60
|
STOP_CODES = frozenset(["fenced", "lease_expired", "authority_changed"])
|
|
56
61
|
|
|
@@ -60,8 +65,6 @@ TERMINAL = frozenset(["completed", "failed_known", "cancelled", "expired"])
|
|
|
60
65
|
NOT_OURS = frozenset(["queued", "claimed", "answered"])
|
|
61
66
|
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
68
|
class WorkContext:
|
|
66
69
|
"""What a handler may do while it works: `progress(note)` posts a note on the request's thread (and renews the
|
|
67
70
|
lease); `begin_effect(summary)` must be called (and must return) before the handler spends or acts outside
|
|
@@ -70,12 +73,12 @@ class WorkContext:
|
|
|
70
73
|
`mandate_covers(call)` whether it covers a call (`mandate_covers`' answer), None when none travelled."""
|
|
71
74
|
|
|
72
75
|
def __init__(self, responder: "Responder", entry: Dict[str, Any], mandate: Optional[Dict[str, Any]] = None,
|
|
73
|
-
caller: Optional[str] = None):
|
|
74
|
-
self._responder, self._entry, self.mandate, self._caller = responder, entry, mandate, caller
|
|
76
|
+
caller: Optional[str] = None, caller_agent: Optional[str] = None):
|
|
77
|
+
self._responder, self._entry, self.mandate, self._caller, self._agent = responder, entry, mandate, caller, caller_agent
|
|
75
78
|
self.stopped = threading.Event()
|
|
76
79
|
|
|
77
80
|
def mandate_covers(self, call: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
78
|
-
return _link.covers(self.mandate, call, self._responder.agent_id, self._responder.wall, self._caller)
|
|
81
|
+
return _link.covers(self.mandate, call, self._responder.agent_id, self._responder.wall, self._caller, self._agent)
|
|
79
82
|
|
|
80
83
|
def progress(self, note: Optional[str] = None) -> None:
|
|
81
84
|
if self.stopped.is_set():
|
|
@@ -100,33 +103,6 @@ class _Work:
|
|
|
100
103
|
Handler = Callable[[BoardRequest, WorkContext], Dict[str, Any]]
|
|
101
104
|
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
def _notice_subject(value: Dict[str, Any], project: str, episode: Optional[str] = None) -> Dict[str, str]:
|
|
105
|
-
"""A notice's subject (`{type, id}`): its request, else its capability, else its episode (a cause such as a block's
|
|
106
|
-
code, so two causes are two episodes for a host, seam r31), else the Project."""
|
|
107
|
-
for kind, given in (("request", value.get("request")), ("capability", value.get("capability")), ("episode", episode)):
|
|
108
|
-
if isinstance(given, str) and 0 < len(given) <= 128:
|
|
109
|
-
return {"type": kind, "id": given}
|
|
110
|
-
return {"type": "project", "id": project}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def _takes_subject(emit: Callable[..., Any]) -> bool:
|
|
114
|
-
"""Whether an emit's signature names a `subject` parameter, as JsonlEventLog.append does. Any other emit (one with
|
|
115
|
-
only **kwargs, which may forward them to a sink without it or set its own, or one whose signature cannot be read)
|
|
116
|
-
is called as 0.55.0 called it, without one (0.56.0 unit r33). A subject already bound into the emit, as with
|
|
117
|
-
functools.partial(log.append, subject=...), stands.
|
|
118
|
-
|
|
119
|
-
The signature is the emit's own, not a wrapped function's: a functools.wraps forwarder that sets its own subject
|
|
120
|
-
would otherwise be passed a second one (0.56.0 unit r34). Nothing here raises, so a notice never stops the
|
|
121
|
-
responder."""
|
|
122
|
-
try:
|
|
123
|
-
params = inspect.signature(emit, follow_wrapped=False).parameters.values()
|
|
124
|
-
return any(p.name == "subject" and p.kind is not inspect.Parameter.VAR_KEYWORD
|
|
125
|
-
and (p.default is inspect.Parameter.empty or p.default is None) for p in params)
|
|
126
|
-
except Exception: # noqa: BLE001 - an unreadable signature (or a default that raises) is called without one
|
|
127
|
-
return False
|
|
128
|
-
|
|
129
|
-
|
|
130
106
|
class Responder(SlotKeeper):
|
|
131
107
|
"""One service's responder for one Project and one or more capabilities, one request at a time.
|
|
132
108
|
|
|
@@ -142,9 +118,11 @@ class Responder(SlotKeeper):
|
|
|
142
118
|
emit: Optional[Callable[..., Any]] = None, service: str = "service",
|
|
143
119
|
now: Callable[[], float] = time.monotonic, rand: Callable[[], float] = random.random,
|
|
144
120
|
spawn: Optional[Callable[[Callable[[], None]], Any]] = None, session_factory: Optional[Callable[..., BoardSession]] = None,
|
|
145
|
-
agent_id: Optional[str] = None, mandate_from: Any = None, wall: Callable[[], float] = time.time
|
|
146
|
-
|
|
147
|
-
|
|
121
|
+
agent_id: Optional[str] = None, mandate_from: Any = None, wall: Callable[[], float] = time.time,
|
|
122
|
+
identify_agent: Optional[Callable[[Dict[str, Any]], Optional[str]]] = None, links: Any = None):
|
|
123
|
+
if admit is None or handler is None or not (identify_agent is None or callable(identify_agent)):
|
|
124
|
+
raise BoardError("invalid_arguments", "A responder needs the service's own input gate (admit) and a handler; "
|
|
125
|
+
"identify_agent, when given, is a function of the sender (FAA-135).")
|
|
148
126
|
caps = list(capabilities)
|
|
149
127
|
if not caps or any(not isinstance(c, str) or not _CAPABILITY.fullmatch(c) for c in caps) or len(set(caps)) != len(caps):
|
|
150
128
|
raise BoardError("invalid_arguments", "Name each capability once, [a-z][a-z0-9._-]{1,127}.")
|
|
@@ -152,8 +130,10 @@ class Responder(SlotKeeper):
|
|
|
152
130
|
raise BoardError("invalid_arguments", "%s is a Project's primary responder's, a session's: a service answers "
|
|
153
131
|
"the capabilities it declares (DEC-0140 PR-1)." % _fs.PRIMARY_CAPABILITY)
|
|
154
132
|
self.hub, self.project = hub, _hub._project(project_id)
|
|
155
|
-
#: FAA-135: read the operator's mandate from a trusted request of his own Project (opt-in; None reads none)
|
|
133
|
+
#: FAA-135: read the operator's mandate from a trusted request of his own Project (opt-in; None reads none), and
|
|
134
|
+
#: bind its chain with the agent id identify_agent names for the sender (asked only when a mandate travelled).
|
|
156
135
|
self.mandate_from, self.agent_id, self.wall = _link.mandate_options(agent_id, mandate_from), agent_id, wall
|
|
136
|
+
self.identify_agent, self.links = identify_agent, _link.notice_links(links) # FAA-187 (21): a link per notify kind
|
|
157
137
|
self.capabilities = caps
|
|
158
138
|
self.credential_file, self.handler, self.admit, self.observe = Path(credential_file), handler, admit, observe
|
|
159
139
|
self.checkpoint = Checkpoint(Path(checkpoint_file))
|
|
@@ -172,10 +152,12 @@ class Responder(SlotKeeper):
|
|
|
172
152
|
self.claim_keys: Dict[str, str] = {}
|
|
173
153
|
self._probing: set = set() # moved slots whose move-back claim lost its answer: asked again under the same key
|
|
174
154
|
self._said: Dict[str, str] = {}
|
|
155
|
+
self._up_since: Optional[float] = None # when the board was last seen up after a fault (FAA-187 (22))
|
|
175
156
|
self._rr = 0
|
|
176
157
|
#: The first pause under an operator's hold (DEC-0079); it doubles to HOLD_RETRY_MAX_SECONDS.
|
|
177
158
|
self.hold_delay = POLL_SECONDS
|
|
178
159
|
self._on_hold: set = set()
|
|
160
|
+
self._taken_back: set = set() # capabilities the operator took back, not yet held again (FAA-325)
|
|
179
161
|
#: One fenced call at a time, and the entry edits beside them (FAA-77, finding 36).
|
|
180
162
|
self._lock = threading.RLock()
|
|
181
163
|
self._to_resume: List[str] = [] # transferred requests waiting until nothing runs (finding 39)
|
|
@@ -203,6 +185,8 @@ class Responder(SlotKeeper):
|
|
|
203
185
|
# goes once, without a subject there (unit r31, seam r31).
|
|
204
186
|
if _takes_subject(self._emit_to):
|
|
205
187
|
extra["subject"] = _notice_subject(value, self.project, episode)
|
|
188
|
+
if notify and kind in getattr(self, "links", {}): # the page that resolves it, which the service named (FAA-187 (21))
|
|
189
|
+
extra["link"] = self.links[kind]
|
|
206
190
|
try:
|
|
207
191
|
self._emit_to(kind, level, text, notify=notify, **extra)
|
|
208
192
|
except Exception: # noqa: BLE001 - an event that cannot be written never stops the responder
|
|
@@ -267,8 +251,8 @@ class Responder(SlotKeeper):
|
|
|
267
251
|
self.slots = {}
|
|
268
252
|
self._recover(status, [c for c in self.capabilities if c in grant.capabilities])
|
|
269
253
|
self.state = "ready"
|
|
270
|
-
for kind in ("board.
|
|
271
|
-
self._said.pop(kind, None)
|
|
254
|
+
for kind in ("board.blocked",) + (() if missing else ("board.access.needed",)):
|
|
255
|
+
self._said.pop(kind, None) # board.down waits for its quiet window (step)
|
|
272
256
|
|
|
273
257
|
def _enroll(self, capability: str) -> None:
|
|
274
258
|
args = {"idempotency": self._idem(self._key("enroll")), "capability": capability}
|
|
@@ -525,6 +509,8 @@ class Responder(SlotKeeper):
|
|
|
525
509
|
self.backoff.reset()
|
|
526
510
|
if self.state == "down": # a lost call was sent again and answered: the session held throughout
|
|
527
511
|
self.state = "ready"
|
|
512
|
+
self._up_since = now if self._up_since is None else self._up_since
|
|
513
|
+
if now - self._up_since >= DOWN_QUIET_SECONDS: # up a whole window: a later outage is said again
|
|
528
514
|
self._said.pop("board.down", None)
|
|
529
515
|
return self.interval()
|
|
530
516
|
except (BoardError, _hub.HubError) as exc:
|
|
@@ -542,7 +528,7 @@ class Responder(SlotKeeper):
|
|
|
542
528
|
return min(renewed) + LEASE_SECONDS - self.now() if renewed and self.session is not None else 0.0
|
|
543
529
|
|
|
544
530
|
def _fault(self, exc: Exception) -> float:
|
|
545
|
-
code, slots = getattr(exc, "code", "unreachable"), self.slots.values()
|
|
531
|
+
code, slots, self._up_since = getattr(exc, "code", "unreachable"), self.slots.values(), None
|
|
546
532
|
connecting = self.state == "connecting" and not any(s.get("state") == "held" for s in slots)
|
|
547
533
|
wait = self.backoff.next()
|
|
548
534
|
left = self._lease_left() - LEASE_MARGIN_SECONDS
|
|
@@ -613,10 +599,11 @@ class Responder(SlotKeeper):
|
|
|
613
599
|
for capability in list(self._suspect): # kept until com.status answered, so it is asked before any enroll
|
|
614
600
|
self._gone(capability)
|
|
615
601
|
self._suspect.discard(capability)
|
|
602
|
+
shared: Dict[str, Any] = {} # one com.status read a tick for every moved slot (FAA-194 (5))
|
|
616
603
|
for capability, slot in list(self.slots.items()):
|
|
617
604
|
if slot.get("state") == "moved": # never enrolled again on its own, even when the new holder lapses (FAA-155)
|
|
618
605
|
if self.now() - slot.get("renewed_at", 0) >= RENEW_SECONDS - 0.5:
|
|
619
|
-
self._moved_back(capability) # unless com.status shows a later generation it may hold (FAA-165)
|
|
606
|
+
self._moved_back(capability, shared) # unless com.status shows a later generation it may hold (FAA-165)
|
|
620
607
|
continue
|
|
621
608
|
if slot.get("state") == "paused":
|
|
622
609
|
if capability != held_by_work and self.now() >= slot.get("retry_at", 0.0) and not self._gone(capability):
|
|
@@ -699,15 +686,14 @@ class Responder(SlotKeeper):
|
|
|
699
686
|
"revision": receipt["request"]["revision"], "step": "claimed", "keys": {}, "effect": False}
|
|
700
687
|
self.checkpoint.put(entry)
|
|
701
688
|
message = self._call("com.get", {"message": entry["message"]})
|
|
702
|
-
|
|
703
|
-
if
|
|
689
|
+
problem = _link.read_back_problem(message, entry, self.project)
|
|
690
|
+
if problem == "digest-mismatch":
|
|
704
691
|
self.checkpoint.drop(entry["request"])
|
|
705
692
|
self._emit("board.request.skipped", "warning", "A claimed request was not accepted: the message read back carries "
|
|
706
693
|
"another digest than the claim. Nothing was done; the claim lapses.", request=entry["request"],
|
|
707
694
|
capability=capability, reason="digest-mismatch")
|
|
708
695
|
return
|
|
709
|
-
if
|
|
710
|
-
or req.get("target") != self.project:
|
|
696
|
+
if problem:
|
|
711
697
|
self.checkpoint.drop(entry["request"])
|
|
712
698
|
self._emit("board.request.skipped", "warning", "A claimed request was not accepted: it is not addressed to this "
|
|
713
699
|
"Project and capability. Nothing was done; the claim lapses.", request=entry["request"],
|
|
@@ -754,12 +740,15 @@ class Responder(SlotKeeper):
|
|
|
754
740
|
self.checkpoint.put(entry)
|
|
755
741
|
return True
|
|
756
742
|
|
|
757
|
-
def _run(self, entry: Dict[str, Any], request: BoardRequest) -> None:
|
|
743
|
+
def _run(self, entry: Dict[str, Any], request: BoardRequest, problem: Optional[str] = None) -> None:
|
|
758
744
|
mandate, unread = _link.read_mandate(request, self.mandate_from, self.agent_id)
|
|
745
|
+
if problem and (mandate is not None or unread): # FAA-196 (1): a resume read back another message than accepted
|
|
746
|
+
mandate, unread = None, problem
|
|
759
747
|
if unread: # by its code alone: the block holds the operator's words
|
|
760
748
|
self._emit("board.request.mandate_unread", "warning", "Request %s carries a mandate the kit does not read (%s); "
|
|
761
749
|
"it is worked without one." % (entry["request"], unread), request=entry["request"], reason=unread)
|
|
762
|
-
|
|
750
|
+
sender = request.sender if isinstance(request.sender, dict) else {}
|
|
751
|
+
ctx = WorkContext(self, entry, mandate, sender.get("project"), _link.sender_agent(self.identify_agent, sender, mandate))
|
|
763
752
|
work = _Work(entry, request, ctx)
|
|
764
753
|
work.renewed_at = self.now()
|
|
765
754
|
self.current = work
|
|
@@ -781,7 +770,7 @@ class Responder(SlotKeeper):
|
|
|
781
770
|
self._complete(entry)
|
|
782
771
|
return
|
|
783
772
|
message = self._call("com.get", {"message": entry["message"]})
|
|
784
|
-
self._run(entry, BoardRequest(message, entry["capability"]))
|
|
773
|
+
self._run(entry, BoardRequest(message, entry["capability"]), _link.read_back_problem(message, entry, self.project))
|
|
785
774
|
|
|
786
775
|
def _fenced(self, op: str, entry: Dict[str, Any], key: str, extra: Optional[Dict[str, Any]] = None,
|
|
787
776
|
apply: Optional[Callable[[Dict[str, Any]], None]] = None) -> Optional[Dict[str, Any]]:
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""The project board's wire (fabric-project-comms/0.1, DEC-0022, DEC-0073, DEC-0077): constants, the refusal codes, the
|
|
2
3
|
contract's request digest, the closed shapes the kit reads back (receipts, refusals, directory pages, messages) and
|
|
3
4
|
evidence. Part of the board connector (FAA-65, layer L4)."""
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""The board responder's slot records (FAA-155, FAA-163): the generation it holds each slot at, a move to another
|
|
2
3
|
session and its take-back, and a capability whose enroll was refused, retried on its own. Part of the board connector
|
|
3
4
|
(FAA-65, layer L4); `Responder` (fabric_board_session) is a `SlotKeeper`."""
|
|
@@ -35,6 +36,10 @@ class SlotKeeper:
|
|
|
35
36
|
self._confirmed[capability] = generation
|
|
36
37
|
self._said.pop("board.slot.refused:" + capability, None) # held again: a later refusal is said again
|
|
37
38
|
self._said.pop("board.slot.contested:" + capability, None) # and a later contest, by enroll or replace
|
|
39
|
+
if capability in self._taken_back: # FAA-325: held again after the operator's take-back, said once
|
|
40
|
+
self._taken_back.discard(capability)
|
|
41
|
+
self._emit("board.slot.returned", "info", "The %s slot on this Project is this service's again: the operator "
|
|
42
|
+
"took it back, and it answers it from now on." % capability, capability=capability, generation=generation)
|
|
38
43
|
|
|
39
44
|
def _note(self, capability: str, record: Optional[Dict[str, Any]]) -> None:
|
|
40
45
|
"""The slot record, best effort: one that cannot be written now is written at the next receipt."""
|
|
@@ -125,19 +130,20 @@ class SlotKeeper:
|
|
|
125
130
|
self._note(capability, dict(held, moved=now)) # said again at the next start if it cannot be written
|
|
126
131
|
if "moved" not in held: # once, even across restarts
|
|
127
132
|
self._emit("board.slot.moved", "warning", "The %s slot on this Project moved to another session. This service stopped answering it; accepted "
|
|
128
|
-
"work stays
|
|
129
|
-
"other session lets it go, or at a restart with accepted work waiting;
|
|
130
|
-
"Fabric
|
|
133
|
+
"work stays accepted (work begun is the new holder's). The operator brings it back by take-back (once the "
|
|
134
|
+
"other session lets it go, or at a restart or reconnect with accepted work waiting; neither alone does) or, "
|
|
135
|
+
"where Fabric can, a move back here. Else ask its maintainer." % capability,
|
|
131
136
|
notify=True, episode=capability, capability=capability, generation=now, held=held["generation"])
|
|
132
137
|
return True
|
|
133
138
|
|
|
134
|
-
def _moved_back(self, capability: str) -> None:
|
|
139
|
+
def _moved_back(self, capability: str, shared: Optional[Dict[str, Any]] = None) -> None:
|
|
135
140
|
"""A moved slot asked about once per later generation (FAA-165): the operator may move it back to this session
|
|
136
141
|
(DEC-0140 PR-2), which this client cannot hear, so when com.status shows a generation past the recorded move, one
|
|
137
142
|
`com.claim` asks whether the slot is its own. A claim that lands makes it held again and starts what it handed
|
|
138
143
|
out; a `lease_expired` one (the move reached this session but its lease lapsed) enrolls again; a fenced one is
|
|
139
144
|
another session's, recorded so the same generation is not asked about twice. Never an enroll or a replace on a
|
|
140
|
-
slot another session holds: a slot moved off stays off unless it is handed back.
|
|
145
|
+
slot another session holds: a slot moved off stays off unless it is handed back. `shared` holds the tick's one
|
|
146
|
+
com.status read, shared by every moved slot (FAA-194 (5))."""
|
|
141
147
|
if self.current is not None or self.stopping:
|
|
142
148
|
return # a claim now could hand out a second request, or start work while the responder stops
|
|
143
149
|
held = self.checkpoint.slots.get(capability) or {}
|
|
@@ -145,7 +151,12 @@ class SlotKeeper:
|
|
|
145
151
|
if held.get("project") != self.project or not _is_int(seen, 1):
|
|
146
152
|
return
|
|
147
153
|
(self.slots.get(capability) or {})["renewed_at"] = self.now()
|
|
148
|
-
status
|
|
154
|
+
if shared is not None and "status" in shared:
|
|
155
|
+
status = shared["status"]
|
|
156
|
+
else:
|
|
157
|
+
status = self._status()
|
|
158
|
+
if shared is not None:
|
|
159
|
+
shared["status"] = status
|
|
149
160
|
now = _whole(next((r.get("generation") for r in status.get("responders") or [] if isinstance(r, dict) and r.get("capability") == capability), None))
|
|
150
161
|
if not _is_int(now, 1) or now <= seen:
|
|
151
162
|
return
|
|
@@ -200,9 +211,12 @@ class SlotKeeper:
|
|
|
200
211
|
if self.checkpoint.slots.get(capability) != held:
|
|
201
212
|
return False # take_back (an action thread) forgot the record meanwhile; its own next tick enrolls
|
|
202
213
|
self.slots[capability] = {"state": "held", "generation": generation, "renewed_at": float("-inf")} # no lease yet
|
|
214
|
+
self._taken_back.discard(capability) # said below as a move back, once
|
|
203
215
|
self._held_at(capability, generation)
|
|
204
216
|
self._said.pop("board.slot.moved", None) # a later move is said again
|
|
205
|
-
|
|
217
|
+
self._emit("board.slot.returned", "info", "The %s slot on this Project is this service's again: the operator moved it "
|
|
218
|
+
"back here, and it answers it from now on." % capability, capability=capability, generation=generation)
|
|
219
|
+
return True # FAA-194 (8): the feed no longer ends on the move's warning
|
|
206
220
|
|
|
207
221
|
def take_back(self, capability: str) -> None:
|
|
208
222
|
"""The operator's act, never the kit's: forget the move; the next tick enrolls once the slot is free (FAA-155)."""
|
|
@@ -212,9 +226,12 @@ class SlotKeeper:
|
|
|
212
226
|
self.checkpoint.slot(capability, None)
|
|
213
227
|
self.claim_keys.pop(capability, None) # a receipt from before the take-back may be dead
|
|
214
228
|
self._probing.discard(capability)
|
|
215
|
-
|
|
229
|
+
state = (self.slots.get(capability) or {}).get("state")
|
|
230
|
+
if state == "moved":
|
|
216
231
|
self.slots[capability] = {"state": "contested", "renewed_at": 0.0}
|
|
217
232
|
self._said.pop("board.slot.moved", None)
|
|
233
|
+
if state != "held": # board.slot.returned is said once the slot is held again (FAA-325); held, nothing to say
|
|
234
|
+
self._taken_back.add(capability)
|
|
218
235
|
|
|
219
236
|
# -- one capability's refusal (FAA-163) ------------------------------------------------------------------------
|
|
220
237
|
def _retry_later(self, capability: str, entries: List[Dict[str, Any]], exc: BoardError) -> None:
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""What an agent uses: `fabric-connections/0.1`, the extension keyed extensions/connections/0.1 (DEC-0096, FAC-SEM-127).
|
|
2
3
|
`connections_view` reads a well-known document as the contract's `connectionsView` does (it never raises);
|
|
3
4
|
`connections_extension` builds the `extensions` entry a service declares, `state` from its own checks and its
|
|
@@ -6,7 +7,7 @@ from __future__ import annotations
|
|
|
6
7
|
|
|
7
8
|
import re
|
|
8
9
|
import urllib.parse
|
|
9
|
-
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence
|
|
10
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
|
|
10
11
|
|
|
11
12
|
import sys as _sys # noqa: E402
|
|
12
13
|
from pathlib import Path as _Path # noqa: E402
|
|
@@ -52,7 +53,8 @@ _KEY_SHAPE = re.compile(
|
|
|
52
53
|
r"(?:^|[^A-Za-z0-9])(?:sk-(?=[A-Za-z0-9_-]*[0-9])(?:(?=[A-Za-z0-9_-]*[A-Z])[A-Za-z0-9_-]{16,}|[A-Za-z0-9_-]{32,})"
|
|
53
54
|
r"|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,}"
|
|
54
55
|
r"|(?:bot)?[0-9]{6,12}:(?=[A-Za-z0-9_-]*[A-Z])(?=[A-Za-z0-9_-]*[a-z])[A-Za-z0-9_-]{34,35}(?![A-Za-z0-9_-]))")
|
|
55
|
-
|
|
56
|
+
_SCHEME_LETTERS = frozenset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
57
|
+
_SCHEME_CHARS = _SCHEME_LETTERS | frozenset("0123456789+.-")
|
|
56
58
|
_CLOSING = ")]}>\"'.,;:!?"
|
|
57
59
|
_TRIED_BACK = 1
|
|
58
60
|
|
|
@@ -139,13 +141,33 @@ def _shape_problems(value: Any) -> List[str]:
|
|
|
139
141
|
return problems
|
|
140
142
|
|
|
141
143
|
|
|
144
|
+
def _scheme_spans(piece: str) -> List[Tuple[int, int]]:
|
|
145
|
+
"""The (start, end) of each URL scheme and its `://` in a piece, where `[a-z][a-z0-9+.-]*://` (case-insensitive,
|
|
146
|
+
ASCII) finds them left to right, read in linear time (FAA-322: the pattern tried every letter of a long run with no
|
|
147
|
+
`://`). From each `://` the run of scheme characters before it is read back, and the match starts at its first
|
|
148
|
+
letter at or after where the last match ended."""
|
|
149
|
+
spans: List[Tuple[int, int]] = []
|
|
150
|
+
after, at = 0, piece.find("://")
|
|
151
|
+
while at >= 0:
|
|
152
|
+
start = at
|
|
153
|
+
while start > after and piece[start - 1] in _SCHEME_CHARS:
|
|
154
|
+
start -= 1
|
|
155
|
+
while start < at and piece[start] not in _SCHEME_LETTERS:
|
|
156
|
+
start += 1
|
|
157
|
+
if start < at:
|
|
158
|
+
spans.append((start, at + 3))
|
|
159
|
+
after = at + 3
|
|
160
|
+
at = piece.find("://", at + 1)
|
|
161
|
+
return spans
|
|
162
|
+
|
|
163
|
+
|
|
142
164
|
def _urls_in(value: str) -> List[str]:
|
|
143
165
|
"""The URLs a member may hold, from every scheme in each whitespace-delimited piece, without the closing brackets,
|
|
144
166
|
quotes and punctuation that end a sentence, and with _TRIED_BACK of them put back (`]` closes an IPv6 host)."""
|
|
145
167
|
urls: List[str] = []
|
|
146
168
|
for piece in _SPACES.split(value):
|
|
147
|
-
for
|
|
148
|
-
url = piece[
|
|
169
|
+
for start, _end in _scheme_spans(piece):
|
|
170
|
+
url = piece[start:]
|
|
149
171
|
end = len(url)
|
|
150
172
|
while end > 0 and url[end - 1] in _CLOSING:
|
|
151
173
|
end -= 1
|
|
@@ -160,8 +182,8 @@ def _long_run(value: str) -> bool:
|
|
|
160
182
|
"""A long secret-shaped run in a piece read whole; in a piece that holds a URL, the text before its first scheme's
|
|
161
183
|
`://` (the URL itself is read by secret_url)."""
|
|
162
184
|
for piece in _SPACES.split(value):
|
|
163
|
-
found =
|
|
164
|
-
cut =
|
|
185
|
+
found = _scheme_spans(piece)
|
|
186
|
+
cut = found[0][1] - 3 if found else -1
|
|
165
187
|
if _long_secret(piece if cut < 0 else piece[:cut]):
|
|
166
188
|
return True
|
|
167
189
|
return False
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""One instance per context (FAA-125, organization row N-11; contract DEC-0082, "One instance per context on a machine"):
|
|
2
3
|
from a context, ``personal`` or ``org:<orgId>``, the service instance, its folders, descriptor and instance lock. Part of
|
|
3
4
|
the service kit (layer L3, after fabric_descriptor); the twin of fabric-context.mjs. Standard library only.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
"""The kit's cryptography, once (FAA-65, layer L1): what the contract's verifiers need and nothing more.
|
|
2
3
|
|
|
3
4
|
- ``ed25519_verify(public_key, message, signature)`` (RFC 8032) and ``is_small_order_key`` with
|