@passioncode-ai/fabric-agent-adapter 0.81.0 → 0.83.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 +200 -3
- package/README.md +11 -6
- package/SECURITY.md +8 -5
- 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/dashboard-links.md +9 -1
- 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 +25 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +19 -1
- 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 +274 -17
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +24 -151
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check_cli.py +164 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +3 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +17 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +24 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -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 +23 -10
- 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 +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +9 -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 +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +28 -4
- 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 +30 -11
- 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_dashboard_link.py +19 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +4 -2
- 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/check_service_channel_agent.py +7 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +22 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a-server.mjs +274 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +14 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +54 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +45 -31
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +16 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +29 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +18 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +6 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +27 -10
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +3 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +21 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +20 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +88 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +24 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ops-digest.mjs +455 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +17 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +18 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +133 -30
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +44 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-socket-proof.mjs +180 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +5 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet-identity.mjs +468 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a.py +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a_server.py +404 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +16 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +97 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +35 -48
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +15 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +35 -159
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +18 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +23 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +43 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +21 -18
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +39 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +55 -96
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +98 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +92 -10
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +43 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ops_digest.py +647 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +16 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +20 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +180 -162
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +274 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_socket_proof.py +390 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +3 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet_identity.py +492 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +465 -55
- 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 +16 -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 +5 -4
- 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/dashboard-links.md +9 -1
- 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 +2 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/admit.py +162 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +19 -1
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +2 -2
- 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 +30 -17
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +53 -35
- 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 +34 -21
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +96 -15
- 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_session.py
CHANGED
|
@@ -3,7 +3,6 @@ accept the exact digest, effect, progress, complete; stop when fenced). The sess
|
|
|
3
3
|
in fabric_board_link and are re-exported here. Part of the board connector (FAA-65, layer L4)."""
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
-
import inspect
|
|
7
6
|
import json
|
|
8
7
|
import random
|
|
9
8
|
import re
|
|
@@ -28,6 +27,8 @@ from fabric_board_link import ( # noqa: E402,F401 - re-exported: callers import
|
|
|
28
27
|
BoardSession,
|
|
29
28
|
Checkpoint,
|
|
30
29
|
PERSONAL_BOARD,
|
|
30
|
+
notice_subject as _notice_subject,
|
|
31
|
+
takes_subject as _takes_subject,
|
|
31
32
|
)
|
|
32
33
|
from fabric_board_slots import ( # noqa: E402,F401 - the slot records (FAA-155, FAA-163); the constants re-exported
|
|
33
34
|
HOLD_RETRY_MAX_SECONDS,
|
|
@@ -51,6 +52,9 @@ IDLE_LEAVE_SECONDS = 3.0
|
|
|
51
52
|
|
|
52
53
|
EVENT_SCHEMA = "urn:fabric:schema:board-responder-event:1"
|
|
53
54
|
|
|
55
|
+
#: `board.down` is said once per outage window: said again only after the board stayed up this long (FAA-187 (22)).
|
|
56
|
+
DOWN_QUIET_SECONDS = 300.0
|
|
57
|
+
|
|
54
58
|
#: On these the attempt is no longer ours: stop acting at once (C4).
|
|
55
59
|
STOP_CODES = frozenset(["fenced", "lease_expired", "authority_changed"])
|
|
56
60
|
|
|
@@ -60,8 +64,6 @@ TERMINAL = frozenset(["completed", "failed_known", "cancelled", "expired"])
|
|
|
60
64
|
NOT_OURS = frozenset(["queued", "claimed", "answered"])
|
|
61
65
|
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
67
|
class WorkContext:
|
|
66
68
|
"""What a handler may do while it works: `progress(note)` posts a note on the request's thread (and renews the
|
|
67
69
|
lease); `begin_effect(summary)` must be called (and must return) before the handler spends or acts outside
|
|
@@ -70,12 +72,12 @@ class WorkContext:
|
|
|
70
72
|
`mandate_covers(call)` whether it covers a call (`mandate_covers`' answer), None when none travelled."""
|
|
71
73
|
|
|
72
74
|
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
|
|
75
|
+
caller: Optional[str] = None, caller_agent: Optional[str] = None):
|
|
76
|
+
self._responder, self._entry, self.mandate, self._caller, self._agent = responder, entry, mandate, caller, caller_agent
|
|
75
77
|
self.stopped = threading.Event()
|
|
76
78
|
|
|
77
79
|
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)
|
|
80
|
+
return _link.covers(self.mandate, call, self._responder.agent_id, self._responder.wall, self._caller, self._agent)
|
|
79
81
|
|
|
80
82
|
def progress(self, note: Optional[str] = None) -> None:
|
|
81
83
|
if self.stopped.is_set():
|
|
@@ -100,33 +102,6 @@ class _Work:
|
|
|
100
102
|
Handler = Callable[[BoardRequest, WorkContext], Dict[str, Any]]
|
|
101
103
|
|
|
102
104
|
|
|
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
105
|
class Responder(SlotKeeper):
|
|
131
106
|
"""One service's responder for one Project and one or more capabilities, one request at a time.
|
|
132
107
|
|
|
@@ -142,9 +117,11 @@ class Responder(SlotKeeper):
|
|
|
142
117
|
emit: Optional[Callable[..., Any]] = None, service: str = "service",
|
|
143
118
|
now: Callable[[], float] = time.monotonic, rand: Callable[[], float] = random.random,
|
|
144
119
|
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
|
-
|
|
120
|
+
agent_id: Optional[str] = None, mandate_from: Any = None, wall: Callable[[], float] = time.time,
|
|
121
|
+
identify_agent: Optional[Callable[[Dict[str, Any]], Optional[str]]] = None, links: Any = None):
|
|
122
|
+
if admit is None or handler is None or not (identify_agent is None or callable(identify_agent)):
|
|
123
|
+
raise BoardError("invalid_arguments", "A responder needs the service's own input gate (admit) and a handler; "
|
|
124
|
+
"identify_agent, when given, is a function of the sender (FAA-135).")
|
|
148
125
|
caps = list(capabilities)
|
|
149
126
|
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
127
|
raise BoardError("invalid_arguments", "Name each capability once, [a-z][a-z0-9._-]{1,127}.")
|
|
@@ -152,8 +129,10 @@ class Responder(SlotKeeper):
|
|
|
152
129
|
raise BoardError("invalid_arguments", "%s is a Project's primary responder's, a session's: a service answers "
|
|
153
130
|
"the capabilities it declares (DEC-0140 PR-1)." % _fs.PRIMARY_CAPABILITY)
|
|
154
131
|
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)
|
|
132
|
+
#: FAA-135: read the operator's mandate from a trusted request of his own Project (opt-in; None reads none), and
|
|
133
|
+
#: bind its chain with the agent id identify_agent names for the sender (asked only when a mandate travelled).
|
|
156
134
|
self.mandate_from, self.agent_id, self.wall = _link.mandate_options(agent_id, mandate_from), agent_id, wall
|
|
135
|
+
self.identify_agent, self.links = identify_agent, _link.notice_links(links) # FAA-187 (21): a link per notify kind
|
|
157
136
|
self.capabilities = caps
|
|
158
137
|
self.credential_file, self.handler, self.admit, self.observe = Path(credential_file), handler, admit, observe
|
|
159
138
|
self.checkpoint = Checkpoint(Path(checkpoint_file))
|
|
@@ -172,6 +151,7 @@ class Responder(SlotKeeper):
|
|
|
172
151
|
self.claim_keys: Dict[str, str] = {}
|
|
173
152
|
self._probing: set = set() # moved slots whose move-back claim lost its answer: asked again under the same key
|
|
174
153
|
self._said: Dict[str, str] = {}
|
|
154
|
+
self._up_since: Optional[float] = None # when the board was last seen up after a fault (FAA-187 (22))
|
|
175
155
|
self._rr = 0
|
|
176
156
|
#: The first pause under an operator's hold (DEC-0079); it doubles to HOLD_RETRY_MAX_SECONDS.
|
|
177
157
|
self.hold_delay = POLL_SECONDS
|
|
@@ -203,6 +183,8 @@ class Responder(SlotKeeper):
|
|
|
203
183
|
# goes once, without a subject there (unit r31, seam r31).
|
|
204
184
|
if _takes_subject(self._emit_to):
|
|
205
185
|
extra["subject"] = _notice_subject(value, self.project, episode)
|
|
186
|
+
if notify and kind in getattr(self, "links", {}): # the page that resolves it, which the service named (FAA-187 (21))
|
|
187
|
+
extra["link"] = self.links[kind]
|
|
206
188
|
try:
|
|
207
189
|
self._emit_to(kind, level, text, notify=notify, **extra)
|
|
208
190
|
except Exception: # noqa: BLE001 - an event that cannot be written never stops the responder
|
|
@@ -267,8 +249,8 @@ class Responder(SlotKeeper):
|
|
|
267
249
|
self.slots = {}
|
|
268
250
|
self._recover(status, [c for c in self.capabilities if c in grant.capabilities])
|
|
269
251
|
self.state = "ready"
|
|
270
|
-
for kind in ("board.
|
|
271
|
-
self._said.pop(kind, None)
|
|
252
|
+
for kind in ("board.blocked",) + (() if missing else ("board.access.needed",)):
|
|
253
|
+
self._said.pop(kind, None) # board.down waits for its quiet window (step)
|
|
272
254
|
|
|
273
255
|
def _enroll(self, capability: str) -> None:
|
|
274
256
|
args = {"idempotency": self._idem(self._key("enroll")), "capability": capability}
|
|
@@ -525,6 +507,8 @@ class Responder(SlotKeeper):
|
|
|
525
507
|
self.backoff.reset()
|
|
526
508
|
if self.state == "down": # a lost call was sent again and answered: the session held throughout
|
|
527
509
|
self.state = "ready"
|
|
510
|
+
self._up_since = now if self._up_since is None else self._up_since
|
|
511
|
+
if now - self._up_since >= DOWN_QUIET_SECONDS: # up a whole window: a later outage is said again
|
|
528
512
|
self._said.pop("board.down", None)
|
|
529
513
|
return self.interval()
|
|
530
514
|
except (BoardError, _hub.HubError) as exc:
|
|
@@ -542,7 +526,7 @@ class Responder(SlotKeeper):
|
|
|
542
526
|
return min(renewed) + LEASE_SECONDS - self.now() if renewed and self.session is not None else 0.0
|
|
543
527
|
|
|
544
528
|
def _fault(self, exc: Exception) -> float:
|
|
545
|
-
code, slots = getattr(exc, "code", "unreachable"), self.slots.values()
|
|
529
|
+
code, slots, self._up_since = getattr(exc, "code", "unreachable"), self.slots.values(), None
|
|
546
530
|
connecting = self.state == "connecting" and not any(s.get("state") == "held" for s in slots)
|
|
547
531
|
wait = self.backoff.next()
|
|
548
532
|
left = self._lease_left() - LEASE_MARGIN_SECONDS
|
|
@@ -613,10 +597,11 @@ class Responder(SlotKeeper):
|
|
|
613
597
|
for capability in list(self._suspect): # kept until com.status answered, so it is asked before any enroll
|
|
614
598
|
self._gone(capability)
|
|
615
599
|
self._suspect.discard(capability)
|
|
600
|
+
shared: Dict[str, Any] = {} # one com.status read a tick for every moved slot (FAA-194 (5))
|
|
616
601
|
for capability, slot in list(self.slots.items()):
|
|
617
602
|
if slot.get("state") == "moved": # never enrolled again on its own, even when the new holder lapses (FAA-155)
|
|
618
603
|
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)
|
|
604
|
+
self._moved_back(capability, shared) # unless com.status shows a later generation it may hold (FAA-165)
|
|
620
605
|
continue
|
|
621
606
|
if slot.get("state") == "paused":
|
|
622
607
|
if capability != held_by_work and self.now() >= slot.get("retry_at", 0.0) and not self._gone(capability):
|
|
@@ -699,15 +684,14 @@ class Responder(SlotKeeper):
|
|
|
699
684
|
"revision": receipt["request"]["revision"], "step": "claimed", "keys": {}, "effect": False}
|
|
700
685
|
self.checkpoint.put(entry)
|
|
701
686
|
message = self._call("com.get", {"message": entry["message"]})
|
|
702
|
-
|
|
703
|
-
if
|
|
687
|
+
problem = _link.read_back_problem(message, entry, self.project)
|
|
688
|
+
if problem == "digest-mismatch":
|
|
704
689
|
self.checkpoint.drop(entry["request"])
|
|
705
690
|
self._emit("board.request.skipped", "warning", "A claimed request was not accepted: the message read back carries "
|
|
706
691
|
"another digest than the claim. Nothing was done; the claim lapses.", request=entry["request"],
|
|
707
692
|
capability=capability, reason="digest-mismatch")
|
|
708
693
|
return
|
|
709
|
-
if
|
|
710
|
-
or req.get("target") != self.project:
|
|
694
|
+
if problem:
|
|
711
695
|
self.checkpoint.drop(entry["request"])
|
|
712
696
|
self._emit("board.request.skipped", "warning", "A claimed request was not accepted: it is not addressed to this "
|
|
713
697
|
"Project and capability. Nothing was done; the claim lapses.", request=entry["request"],
|
|
@@ -754,12 +738,15 @@ class Responder(SlotKeeper):
|
|
|
754
738
|
self.checkpoint.put(entry)
|
|
755
739
|
return True
|
|
756
740
|
|
|
757
|
-
def _run(self, entry: Dict[str, Any], request: BoardRequest) -> None:
|
|
741
|
+
def _run(self, entry: Dict[str, Any], request: BoardRequest, problem: Optional[str] = None) -> None:
|
|
758
742
|
mandate, unread = _link.read_mandate(request, self.mandate_from, self.agent_id)
|
|
743
|
+
if problem and (mandate is not None or unread): # FAA-196 (1): a resume read back another message than accepted
|
|
744
|
+
mandate, unread = None, problem
|
|
759
745
|
if unread: # by its code alone: the block holds the operator's words
|
|
760
746
|
self._emit("board.request.mandate_unread", "warning", "Request %s carries a mandate the kit does not read (%s); "
|
|
761
747
|
"it is worked without one." % (entry["request"], unread), request=entry["request"], reason=unread)
|
|
762
|
-
|
|
748
|
+
sender = request.sender if isinstance(request.sender, dict) else {}
|
|
749
|
+
ctx = WorkContext(self, entry, mandate, sender.get("project"), _link.sender_agent(self.identify_agent, sender, mandate))
|
|
763
750
|
work = _Work(entry, request, ctx)
|
|
764
751
|
work.renewed_at = self.now()
|
|
765
752
|
self.current = work
|
|
@@ -781,7 +768,7 @@ class Responder(SlotKeeper):
|
|
|
781
768
|
self._complete(entry)
|
|
782
769
|
return
|
|
783
770
|
message = self._call("com.get", {"message": entry["message"]})
|
|
784
|
-
self._run(entry, BoardRequest(message, entry["capability"]))
|
|
771
|
+
self._run(entry, BoardRequest(message, entry["capability"]), _link.read_back_problem(message, entry, self.project))
|
|
785
772
|
|
|
786
773
|
def _fenced(self, op: str, entry: Dict[str, Any], key: str, extra: Optional[Dict[str, Any]] = None,
|
|
787
774
|
apply: Optional[Callable[[Dict[str, Any]], None]] = None) -> Optional[Dict[str, Any]]:
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py
CHANGED
|
@@ -125,19 +125,20 @@ class SlotKeeper:
|
|
|
125
125
|
self._note(capability, dict(held, moved=now)) # said again at the next start if it cannot be written
|
|
126
126
|
if "moved" not in held: # once, even across restarts
|
|
127
127
|
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
|
|
128
|
+
"work stays accepted (work begun is the new holder's). The operator brings it back by take-back (once the "
|
|
129
|
+
"other session lets it go, or at a restart or reconnect with accepted work waiting; neither alone does) or, "
|
|
130
|
+
"where Fabric can, a move back here. Else ask its maintainer." % capability,
|
|
131
131
|
notify=True, episode=capability, capability=capability, generation=now, held=held["generation"])
|
|
132
132
|
return True
|
|
133
133
|
|
|
134
|
-
def _moved_back(self, capability: str) -> None:
|
|
134
|
+
def _moved_back(self, capability: str, shared: Optional[Dict[str, Any]] = None) -> None:
|
|
135
135
|
"""A moved slot asked about once per later generation (FAA-165): the operator may move it back to this session
|
|
136
136
|
(DEC-0140 PR-2), which this client cannot hear, so when com.status shows a generation past the recorded move, one
|
|
137
137
|
`com.claim` asks whether the slot is its own. A claim that lands makes it held again and starts what it handed
|
|
138
138
|
out; a `lease_expired` one (the move reached this session but its lease lapsed) enrolls again; a fenced one is
|
|
139
139
|
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.
|
|
140
|
+
slot another session holds: a slot moved off stays off unless it is handed back. `shared` holds the tick's one
|
|
141
|
+
com.status read, shared by every moved slot (FAA-194 (5))."""
|
|
141
142
|
if self.current is not None or self.stopping:
|
|
142
143
|
return # a claim now could hand out a second request, or start work while the responder stops
|
|
143
144
|
held = self.checkpoint.slots.get(capability) or {}
|
|
@@ -145,7 +146,12 @@ class SlotKeeper:
|
|
|
145
146
|
if held.get("project") != self.project or not _is_int(seen, 1):
|
|
146
147
|
return
|
|
147
148
|
(self.slots.get(capability) or {})["renewed_at"] = self.now()
|
|
148
|
-
status
|
|
149
|
+
if shared is not None and "status" in shared:
|
|
150
|
+
status = shared["status"]
|
|
151
|
+
else:
|
|
152
|
+
status = self._status()
|
|
153
|
+
if shared is not None:
|
|
154
|
+
shared["status"] = status
|
|
149
155
|
now = _whole(next((r.get("generation") for r in status.get("responders") or [] if isinstance(r, dict) and r.get("capability") == capability), None))
|
|
150
156
|
if not _is_int(now, 1) or now <= seen:
|
|
151
157
|
return
|
|
@@ -202,7 +208,9 @@ class SlotKeeper:
|
|
|
202
208
|
self.slots[capability] = {"state": "held", "generation": generation, "renewed_at": float("-inf")} # no lease yet
|
|
203
209
|
self._held_at(capability, generation)
|
|
204
210
|
self._said.pop("board.slot.moved", None) # a later move is said again
|
|
205
|
-
|
|
211
|
+
self._emit("board.slot.returned", "info", "The %s slot on this Project is this service's again: the operator moved it "
|
|
212
|
+
"back here, and it answers it from now on." % capability, capability=capability, generation=generation)
|
|
213
|
+
return True # FAA-194 (8): the feed no longer ends on the move's warning
|
|
206
214
|
|
|
207
215
|
def take_back(self, capability: str) -> None:
|
|
208
216
|
"""The operator's act, never the kit's: forget the move; the next tick enrolls once the slot is free (FAA-155)."""
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py
CHANGED
|
@@ -4,12 +4,9 @@
|
|
|
4
4
|
`degraded[]`. Part of the service kit (FAA-62, layer L3). Standard library only."""
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
import bisect
|
|
8
|
-
import ipaddress
|
|
9
7
|
import re
|
|
10
|
-
import unicodedata
|
|
11
8
|
import urllib.parse
|
|
12
|
-
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence
|
|
9
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
|
|
13
10
|
|
|
14
11
|
import sys as _sys # noqa: E402
|
|
15
12
|
from pathlib import Path as _Path # noqa: E402
|
|
@@ -18,7 +15,12 @@ if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from
|
|
|
18
15
|
|
|
19
16
|
from fabric_base import ServiceError # noqa: E402
|
|
20
17
|
from fabric_secrets import ( # noqa: E402
|
|
21
|
-
|
|
18
|
+
_BEARER_INSIDE as _BEARER, _long_secret, credential_name, has_jwt, raw_values, secret_argument, secret_value,
|
|
19
|
+
)
|
|
20
|
+
# The WHATWG host reading and its IDNA tables (FAA-98) live in fabric_secrets since FAA-309, where secret_url reads a
|
|
21
|
+
# URL's host with them; the names stay here for code and tests that reached them.
|
|
22
|
+
from fabric_secrets import ( # noqa: E402,F401
|
|
23
|
+
_FORBIDDEN_DOMAIN, _FORBIDDEN_HOST, _IDNA_DISALLOWED, _IDNA_IGNORED, _domain_ok, _idna_host, _ipv4_number, _ipv6_ok,
|
|
22
24
|
)
|
|
23
25
|
|
|
24
26
|
CONNECTIONS_EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/connections/0.1"
|
|
@@ -50,7 +52,8 @@ _KEY_SHAPE = re.compile(
|
|
|
50
52
|
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,})"
|
|
51
53
|
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,}"
|
|
52
54
|
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_-]))")
|
|
53
|
-
|
|
55
|
+
_SCHEME_LETTERS = frozenset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
56
|
+
_SCHEME_CHARS = _SCHEME_LETTERS | frozenset("0123456789+.-")
|
|
54
57
|
_CLOSING = ")]}>\"'.,;:!?"
|
|
55
58
|
_TRIED_BACK = 1
|
|
56
59
|
|
|
@@ -137,13 +140,33 @@ def _shape_problems(value: Any) -> List[str]:
|
|
|
137
140
|
return problems
|
|
138
141
|
|
|
139
142
|
|
|
143
|
+
def _scheme_spans(piece: str) -> List[Tuple[int, int]]:
|
|
144
|
+
"""The (start, end) of each URL scheme and its `://` in a piece, where `[a-z][a-z0-9+.-]*://` (case-insensitive,
|
|
145
|
+
ASCII) finds them left to right, read in linear time (FAA-322: the pattern tried every letter of a long run with no
|
|
146
|
+
`://`). From each `://` the run of scheme characters before it is read back, and the match starts at its first
|
|
147
|
+
letter at or after where the last match ended."""
|
|
148
|
+
spans: List[Tuple[int, int]] = []
|
|
149
|
+
after, at = 0, piece.find("://")
|
|
150
|
+
while at >= 0:
|
|
151
|
+
start = at
|
|
152
|
+
while start > after and piece[start - 1] in _SCHEME_CHARS:
|
|
153
|
+
start -= 1
|
|
154
|
+
while start < at and piece[start] not in _SCHEME_LETTERS:
|
|
155
|
+
start += 1
|
|
156
|
+
if start < at:
|
|
157
|
+
spans.append((start, at + 3))
|
|
158
|
+
after = at + 3
|
|
159
|
+
at = piece.find("://", at + 1)
|
|
160
|
+
return spans
|
|
161
|
+
|
|
162
|
+
|
|
140
163
|
def _urls_in(value: str) -> List[str]:
|
|
141
164
|
"""The URLs a member may hold, from every scheme in each whitespace-delimited piece, without the closing brackets,
|
|
142
165
|
quotes and punctuation that end a sentence, and with _TRIED_BACK of them put back (`]` closes an IPv6 host)."""
|
|
143
166
|
urls: List[str] = []
|
|
144
167
|
for piece in _SPACES.split(value):
|
|
145
|
-
for
|
|
146
|
-
url = piece[
|
|
168
|
+
for start, _end in _scheme_spans(piece):
|
|
169
|
+
url = piece[start:]
|
|
147
170
|
end = len(url)
|
|
148
171
|
while end > 0 and url[end - 1] in _CLOSING:
|
|
149
172
|
end -= 1
|
|
@@ -158,8 +181,8 @@ def _long_run(value: str) -> bool:
|
|
|
158
181
|
"""A long secret-shaped run in a piece read whole; in a piece that holds a URL, the text before its first scheme's
|
|
159
182
|
`://` (the URL itself is read by secret_url)."""
|
|
160
183
|
for piece in _SPACES.split(value):
|
|
161
|
-
found =
|
|
162
|
-
cut =
|
|
184
|
+
found = _scheme_spans(piece)
|
|
185
|
+
cut = found[0][1] - 3 if found else -1
|
|
163
186
|
if _long_secret(piece if cut < 0 else piece[:cut]):
|
|
164
187
|
return True
|
|
165
188
|
return False
|
|
@@ -171,153 +194,6 @@ _credential_name, _secret_value, _secret_argument = credential_name, secret_valu
|
|
|
171
194
|
|
|
172
195
|
|
|
173
196
|
_SPECIAL = ("http", "https", "ws", "wss", "ftp", "file")
|
|
174
|
-
_FORBIDDEN_HOST = set("\x00\t\n\r #/:<>?@[\\]^|")
|
|
175
|
-
_FORBIDDEN_DOMAIN = _FORBIDDEN_HOST | set(chr(c) for c in range(0x20)) | {"%", "\x7f"}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
# FAA-98: UTS46 narrowed, not ported. A non-ASCII host is refused only where every Node from 20 to 26 refuses it, so
|
|
179
|
-
# the doubt stays on the closed side (its URL's credentials are read). The two tables are measured by
|
|
180
|
-
# test/fixtures/idna/make_idna_tables.mjs (idna-tables.json, held equal by test_the_idna_tables_are_the_measured_ones):
|
|
181
|
-
# code points refused in every context, and code points removed between letters. The contextual rules read Unicode 3.2
|
|
182
|
-
# classes (`unicodedata.ucd_3_2_0`) and skip what that Unicode does not know, since Node's own UTS46 data lags its
|
|
183
|
-
# Unicode: an Arabic letter of Unicode 14 is no right-to-left letter to it.
|
|
184
|
-
_IDNA_DISALLOWED = (
|
|
185
|
-
(0x80, 0xa0), (0xa8, 0xa8), (0xaf, 0xaf), (0xb4, 0xb4), (0xb8, 0xb8), (0x2d8, 0x2dd), (0x37a, 0x37a),
|
|
186
|
-
(0x384, 0x385), (0x600, 0x605), (0x61c, 0x61c), (0x6dd, 0x6dd), (0x70f, 0x70f), (0x890, 0x891), (0x8e2, 0x8e2),
|
|
187
|
-
(0x1680, 0x1680), (0x1fbd, 0x1fbd), (0x1fbf, 0x1fc1), (0x1fcd, 0x1fcf), (0x1fdd, 0x1fdf), (0x1fed, 0x1fee),
|
|
188
|
-
(0x1ffd, 0x1ffe), (0x2000, 0x200a), (0x200e, 0x200f), (0x2017, 0x2017), (0x2024, 0x2026), (0x2028, 0x202f),
|
|
189
|
-
(0x203e, 0x203e), (0x2047, 0x2049), (0x205f, 0x205f), (0x2066, 0x2069), (0x2100, 0x2101), (0x2105, 0x2106),
|
|
190
|
-
(0x2488, 0x249b), (0x2a74, 0x2a74), (0x2ff0, 0x3000), (0x309b, 0x309c), (0x31ef, 0x31ef), (0x33c2, 0x33c2),
|
|
191
|
-
(0x33c7, 0x33c7), (0x33d8, 0x33d8), (0xe000, 0xf8ff), (0xfc5e, 0xfc63), (0xfdfa, 0xfdfb), (0xfe12, 0xfe13),
|
|
192
|
-
(0xfe16, 0xfe16), (0xfe19, 0xfe19), (0xfe30, 0xfe30), (0xfe47, 0xfe4c), (0xfe52, 0xfe52), (0xfe55, 0xfe56),
|
|
193
|
-
(0xfe5f, 0xfe5f), (0xfe64, 0xfe65), (0xfe68, 0xfe68), (0xfe6a, 0xfe6b), (0xfe70, 0xfe70), (0xfe72, 0xfe72),
|
|
194
|
-
(0xfe74, 0xfe74), (0xfe76, 0xfe76), (0xfe78, 0xfe78), (0xfe7a, 0xfe7a), (0xfe7c, 0xfe7c), (0xfe7e, 0xfe7e),
|
|
195
|
-
(0xff03, 0xff03), (0xff05, 0xff05), (0xff0f, 0xff0f), (0xff1a, 0xff1a), (0xff1c, 0xff1c), (0xff1e, 0xff20),
|
|
196
|
-
(0xff3b, 0xff3e), (0xff5c, 0xff5c), (0xffe3, 0xffe3), (0xfff9, 0xfffd), (0x110bd, 0x110bd), (0x110cd, 0x110cd),
|
|
197
|
-
(0x13430, 0x1343f), (0x1f100, 0x1f100), (0xe0001, 0xe0001), (0xe0020, 0xe007f), (0xf0000, 0xffffd),
|
|
198
|
-
(0x100000, 0x10fffd),
|
|
199
|
-
)
|
|
200
|
-
_IDNA_IGNORED = (
|
|
201
|
-
(0xad, 0xad), (0x34f, 0x34f), (0x180b, 0x180d), (0x180f, 0x180f), (0x200b, 0x200b), (0x2060, 0x2060),
|
|
202
|
-
(0x2064, 0x2064), (0xfe00, 0xfe0f), (0xfeff, 0xfeff), (0x1bca0, 0x1bca3), (0xe0100, 0xe01ef),
|
|
203
|
-
)
|
|
204
|
-
_IDNA_DOTS = re.compile("[.\u3002\uff0e\uff61]")
|
|
205
|
-
_U32 = unicodedata.ucd_3_2_0
|
|
206
|
-
_RTL_OK = {"R", "AL", "AN", "EN", "ES", "CS", "ET", "ON", "BN", "NSM"}
|
|
207
|
-
_JOINERS = "\u200c\u200d"
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def _in_ranges(ranges: Sequence[tuple], cp: int) -> bool:
|
|
211
|
-
i = bisect.bisect_right(ranges, (cp, 0x10ffff)) - 1
|
|
212
|
-
return i >= 0 and ranges[i][0] <= cp <= ranges[i][1]
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def _known(c: str) -> bool:
|
|
216
|
-
return _U32.category(c) != "Cn" and unicodedata.category(c) != "Cn"
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def _joiner_refused(label: str, i: int) -> bool:
|
|
220
|
-
"""CheckJoiners as Node 20 to 26 all read it: they find a joining context, or a virama, anywhere in the label
|
|
221
|
-
(`\u0628a\u200c\u0627` and `\u0628\u200c\u0627\u200d` parse), so only a joiner with nothing but ASCII beside it
|
|
222
|
-
in its label has none for certain."""
|
|
223
|
-
return all(c.isascii() for j, c in enumerate(label) if j != i)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def _bidi_refused(label: str) -> bool:
|
|
227
|
-
"""RFC 5893 as Node 20 to 26 all read it: only a label holding R, AL or AN is judged, and its first code point is
|
|
228
|
-
not (Node 26 alone refuses a label opening with a digit or a hyphen)."""
|
|
229
|
-
if not label or not all(_known(c) for c in label) or any(c in _JOINERS for c in label):
|
|
230
|
-
return False # Node 20 to 26 pass a label holding a joiner over (`\u05d0a\u094d\u200d` parses)
|
|
231
|
-
k = [_U32.bidirectional(c) for c in label]
|
|
232
|
-
if not any(x in ("R", "AL", "AN") for x in k):
|
|
233
|
-
return False
|
|
234
|
-
body = list(k)
|
|
235
|
-
while body and body[-1] == "NSM":
|
|
236
|
-
body.pop()
|
|
237
|
-
if not body or body[-1] not in ("R", "AL", "EN", "AN"):
|
|
238
|
-
return True
|
|
239
|
-
return k[0] in ("R", "AL") and (any(x not in _RTL_OK for x in k) or ("EN" in k and "AN" in k))
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def _idna_host(host: str) -> Optional[str]:
|
|
243
|
-
"""None where WHATWG's domain-to-ASCII refuses a non-ASCII host for certain; else the host as UTS46 maps it, read
|
|
244
|
-
as NFKC with case folding (exact enough for the ASCII rules that follow, which read digits and dots)."""
|
|
245
|
-
if any(_in_ranges(_IDNA_DISALLOWED, ord(c)) for c in host):
|
|
246
|
-
return None
|
|
247
|
-
kept = "".join(c for c in host if not _in_ranges(_IDNA_IGNORED, ord(c)))
|
|
248
|
-
if kept == "":
|
|
249
|
-
return None # nothing left: an empty host
|
|
250
|
-
if not all(_known(c) for c in kept):
|
|
251
|
-
return host # a code point this Python's Unicode does not know: the contextual rules are not read
|
|
252
|
-
mapped = unicodedata.normalize("NFC", unicodedata.normalize("NFKC", unicodedata.normalize("NFKC", kept).casefold()))
|
|
253
|
-
for label in _IDNA_DOTS.split(mapped):
|
|
254
|
-
first = label[:1]
|
|
255
|
-
if first and first not in "\u17b4\u17b5" and _U32.category(first)[0] == "M" and unicodedata.category(first)[0] == "M":
|
|
256
|
-
return None # a label opening with a mark (U+17B4 and U+17B5 are ignored by the newer Nodes)
|
|
257
|
-
if any(c in _JOINERS and _joiner_refused(label, i) for i, c in enumerate(label)):
|
|
258
|
-
return None
|
|
259
|
-
if _bidi_refused(label):
|
|
260
|
-
return None
|
|
261
|
-
return _IDNA_DOTS.sub(".", mapped)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def _ipv4_number(part: str) -> Optional[int]:
|
|
265
|
-
if part == "":
|
|
266
|
-
return None # an empty part fails WHATWG's IPv4 parser (`1..2`); `0x` alone is 0
|
|
267
|
-
base = 10
|
|
268
|
-
if part[:2].lower() == "0x":
|
|
269
|
-
part, base = part[2:], 16
|
|
270
|
-
elif len(part) > 1 and part[0] == "0":
|
|
271
|
-
part, base = part[1:], 8
|
|
272
|
-
if part == "":
|
|
273
|
-
return 0
|
|
274
|
-
digits = {10: "0123456789", 16: "0123456789abcdefABCDEF", 8: "01234567"}[base]
|
|
275
|
-
return int(part, base) if all(c in digits for c in part) else None
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def _domain_ok(host: str) -> bool:
|
|
279
|
-
"""The WHATWG host parser on a special scheme's host, enough to know when `new URL` refuses it."""
|
|
280
|
-
if host.startswith("["):
|
|
281
|
-
return host.endswith("]") and _ipv6_ok(host[1:-1])
|
|
282
|
-
if re.search(r"%(?![0-9A-Fa-f]{2})", host) is None:
|
|
283
|
-
try:
|
|
284
|
-
host = urllib.parse.unquote_to_bytes(host).decode("utf-8")
|
|
285
|
-
except UnicodeDecodeError:
|
|
286
|
-
return False
|
|
287
|
-
if any(c in _FORBIDDEN_DOMAIN for c in host):
|
|
288
|
-
return False
|
|
289
|
-
# A non-ASCII host goes through UTS46 in WHATWG; this module reads it narrowly (_idna_host, FAA-98). When unsure,
|
|
290
|
-
# the host is taken as valid, so its URL's credentials are read: the doubt then hides a list rather than publish one
|
|
291
|
-
# (round-2 reading). A host UTS46 maps to ASCII is read by the ASCII rules below (`127.0.0.1` is an address).
|
|
292
|
-
if any(ord(c) > 0x7f for c in host):
|
|
293
|
-
mapped = _idna_host(host)
|
|
294
|
-
if mapped is None:
|
|
295
|
-
return False
|
|
296
|
-
if not mapped.isascii() or any(c in _FORBIDDEN_DOMAIN for c in mapped):
|
|
297
|
-
return True
|
|
298
|
-
host = mapped
|
|
299
|
-
labels = host.split(".")
|
|
300
|
-
if labels and labels[-1] == "" and len(labels) > 1:
|
|
301
|
-
labels = labels[:-1]
|
|
302
|
-
last = labels[-1] if labels else ""
|
|
303
|
-
if not (last and (last.isdigit() and last.isascii() or re.fullmatch(r"0[xX][0-9A-Fa-f]*", last))):
|
|
304
|
-
return host != ""
|
|
305
|
-
numbers = [_ipv4_number(label) for label in labels]
|
|
306
|
-
if len(numbers) > 4 or any(n is None for n in numbers):
|
|
307
|
-
return False
|
|
308
|
-
return all(n <= 255 for n in numbers[:-1]) and numbers[-1] < 256 ** (5 - len(numbers))
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
def _ipv6_ok(text: str) -> bool:
|
|
312
|
-
if "%" in text:
|
|
313
|
-
return False # a zone id: `ipaddress` takes one, WHATWG does not
|
|
314
|
-
try:
|
|
315
|
-
ipaddress.IPv6Address(text)
|
|
316
|
-
return True
|
|
317
|
-
except ValueError:
|
|
318
|
-
return False
|
|
319
|
-
|
|
320
|
-
|
|
321
197
|
_SURROGATE = re.compile("[\ud800-\udfff]")
|
|
322
198
|
_QUERY_ENCODE = set(chr(c) for c in range(0x21)) | set('"#<>')
|
|
323
199
|
_FRAGMENT_ENCODE = set(chr(c) for c in range(0x21)) | set('"<>`')
|
|
@@ -388,7 +264,7 @@ def _whatwg_parts(value: str) -> Optional[Dict[str, str]]:
|
|
|
388
264
|
return None
|
|
389
265
|
elif any(c in _FORBIDDEN_HOST for c in host):
|
|
390
266
|
return None
|
|
391
|
-
elif host == "" and ("@" in authority or
|
|
267
|
+
elif host == "" and ("@" in authority or ":" in hostport):
|
|
392
268
|
return None
|
|
393
269
|
return {"password": password, "query": query, "fragment": fragment}
|
|
394
270
|
|
|
@@ -424,7 +300,7 @@ def _secret_url(value: str) -> bool:
|
|
|
424
300
|
|
|
425
301
|
|
|
426
302
|
def _credential(value: str, runs: bool) -> bool:
|
|
427
|
-
return bool(
|
|
303
|
+
return bool(has_jwt(value) or _KEY_SHAPE.search(value) or _BEARER.search(value) # has_jwt: one pass (FAA-313)
|
|
428
304
|
or (runs and _long_run(value)) or any(_secret_url(url) for url in _urls_in(value)))
|
|
429
305
|
|
|
430
306
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py
CHANGED
|
@@ -46,6 +46,8 @@ from fabric_jsvalue import ( # noqa: E402 - the core (FAA-65)
|
|
|
46
46
|
)
|
|
47
47
|
from fabric_paths import atomic_write as _atomic_write # noqa: E402 - the kit's private writer (FAA-85)
|
|
48
48
|
import fabric_peer_owner as _po # noqa: E402 - a loopback key set is read from this account's listener only (FAA-255)
|
|
49
|
+
import fabric_secrets # noqa: E402 - the WHATWG host reading (FAA-98, FAA-309)
|
|
50
|
+
import fabric_socket_proof # noqa: E402 - the one loopback host rule (FAA-187 (15))
|
|
49
51
|
from fabric_mandate import effect_closure # noqa: E402,F401 - defined once beside the mandate that reads it too (DEC-0125)
|
|
50
52
|
|
|
51
53
|
REFUSAL_CODES = (
|
|
@@ -621,16 +623,10 @@ def action_digest(action: Any) -> str:
|
|
|
621
623
|
|
|
622
624
|
# --- the estate's keys ----------------------------------------------------------------------------------------
|
|
623
625
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
address = ipaddress.ip_address(host)
|
|
629
|
-
except ValueError:
|
|
630
|
-
return False
|
|
631
|
-
# A zone (`::1%eth0`) is no loopback: urllib decodes `%3A80` in it into another address (0.56.0 seam r13).
|
|
632
|
-
# Nor an IPv4-mapped address, which Python 3.13+ counts as loopback and 3.9 and Node do not (0.56.0 seam r14).
|
|
633
|
-
return address.is_loopback and not getattr(address, "scope_id", None) and not getattr(address, "ipv4_mapped", None)
|
|
626
|
+
#: A key set source's loopback host, the kit's one rule (fabric_socket_proof.loopback_host), shared with fabric_http's
|
|
627
|
+
#: check_url, so a source starts a service on both kits alike (FAA-187 (15); `127.1`, `2130706433`, a name under
|
|
628
|
+
#: `.localhost`). A zone and an IPv4-mapped address stay refused, as before (0.56.0 seam r13, r14).
|
|
629
|
+
_loopback = fabric_socket_proof.loopback_host
|
|
634
630
|
|
|
635
631
|
|
|
636
632
|
# fabric_http._URL_ENDS (this module may not import fabric_http; test_fabric_delegation pins them equal).
|
|
@@ -663,7 +659,16 @@ def _source_authority_ok(parts: urllib.parse.SplitResult) -> bool:
|
|
|
663
659
|
parts.port # noqa: B018 - raises on a port above 65535
|
|
664
660
|
except ValueError:
|
|
665
661
|
return False
|
|
666
|
-
|
|
662
|
+
# A host `new URL` refuses for its structure (`a127.0.0.1`, `09`), or one Python's IDNA codec cannot write for
|
|
663
|
+
# http.client (U+180E), is refused here as check_url refuses it, never read and failed on every call (FAA-187 (14),
|
|
664
|
+
# (16)).
|
|
665
|
+
host = parts.hostname or ""
|
|
666
|
+
try:
|
|
667
|
+
if not host.isascii():
|
|
668
|
+
host.encode("idna")
|
|
669
|
+
except UnicodeError:
|
|
670
|
+
return False
|
|
671
|
+
return bool(host) and not fabric_secrets.url_refused(parts.geturl())
|
|
667
672
|
|
|
668
673
|
|
|
669
674
|
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
@@ -734,7 +739,7 @@ def _fetch(source: str) -> Any:
|
|
|
734
739
|
body += chunk
|
|
735
740
|
if deadline["fired"] or time.monotonic() > deadline["end"]:
|
|
736
741
|
raise TimeoutError("the key set did not arrive within %s s" % JWKS_FETCH_SECONDS)
|
|
737
|
-
return json.loads(body[:65536])
|
|
742
|
+
return json.loads(body[:65536], parse_constant=_no_constant) # NaN is no JSON, as JSON.parse reads it (FAA-194)
|
|
738
743
|
except urllib.error.HTTPError as exc:
|
|
739
744
|
exc.close()
|
|
740
745
|
raise
|
|
@@ -745,7 +750,7 @@ def _fetch(source: str) -> Any:
|
|
|
745
750
|
finally:
|
|
746
751
|
if deadline["timer"] is not None:
|
|
747
752
|
deadline["timer"].cancel()
|
|
748
|
-
return json.loads(Path(source).expanduser().read_text())
|
|
753
|
+
return json.loads(Path(source).expanduser().read_text(), parse_constant=_no_constant)
|
|
749
754
|
|
|
750
755
|
|
|
751
756
|
class JwksCache:
|