@passioncode-ai/fabric-agent-adapter 0.82.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 +122 -3
- package/README.md +6 -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_provider.py +31 -15
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +37 -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 +3 -3
- 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 +9 -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 +20 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
- 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 +5 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +8 -5
- 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 +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 +26 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +3 -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 +21 -7
- 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 +6 -2
- 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 +15 -2
- 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 +55 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +15 -2
- 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 +27 -6
- 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 +31 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +18 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +9 -5
- 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 +15 -6
- 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 +93 -93
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +37 -12
- 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 +365 -53
- 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/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/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_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
|
@@ -6,7 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
import re
|
|
8
8
|
import urllib.parse
|
|
9
|
-
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence
|
|
9
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
|
|
10
10
|
|
|
11
11
|
import sys as _sys # noqa: E402
|
|
12
12
|
from pathlib import Path as _Path # noqa: E402
|
|
@@ -52,7 +52,8 @@ _KEY_SHAPE = re.compile(
|
|
|
52
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,})"
|
|
53
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,}"
|
|
54
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_-]))")
|
|
55
|
-
|
|
55
|
+
_SCHEME_LETTERS = frozenset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
56
|
+
_SCHEME_CHARS = _SCHEME_LETTERS | frozenset("0123456789+.-")
|
|
56
57
|
_CLOSING = ")]}>\"'.,;:!?"
|
|
57
58
|
_TRIED_BACK = 1
|
|
58
59
|
|
|
@@ -139,13 +140,33 @@ def _shape_problems(value: Any) -> List[str]:
|
|
|
139
140
|
return problems
|
|
140
141
|
|
|
141
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
|
+
|
|
142
163
|
def _urls_in(value: str) -> List[str]:
|
|
143
164
|
"""The URLs a member may hold, from every scheme in each whitespace-delimited piece, without the closing brackets,
|
|
144
165
|
quotes and punctuation that end a sentence, and with _TRIED_BACK of them put back (`]` closes an IPv6 host)."""
|
|
145
166
|
urls: List[str] = []
|
|
146
167
|
for piece in _SPACES.split(value):
|
|
147
|
-
for
|
|
148
|
-
url = piece[
|
|
168
|
+
for start, _end in _scheme_spans(piece):
|
|
169
|
+
url = piece[start:]
|
|
149
170
|
end = len(url)
|
|
150
171
|
while end > 0 and url[end - 1] in _CLOSING:
|
|
151
172
|
end -= 1
|
|
@@ -160,8 +181,8 @@ def _long_run(value: str) -> bool:
|
|
|
160
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
|
|
161
182
|
`://` (the URL itself is read by secret_url)."""
|
|
162
183
|
for piece in _SPACES.split(value):
|
|
163
|
-
found =
|
|
164
|
-
cut =
|
|
184
|
+
found = _scheme_spans(piece)
|
|
185
|
+
cut = found[0][1] - 3 if found else -1
|
|
165
186
|
if _long_secret(piece if cut < 0 else piece[:cut]):
|
|
166
187
|
return True
|
|
167
188
|
return False
|
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:
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py
CHANGED
|
@@ -55,7 +55,7 @@ from fabric_usage import (
|
|
|
55
55
|
DASHBOARD_EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1"
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
DASHBOARD_KIT = "fabric-dashboard-kit@1.1.
|
|
58
|
+
DASHBOARD_KIT = "fabric-dashboard-kit@1.1.8"
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
REFERENCE_THEMES = ("graphite", "fabric", "paper", "forest", "ocean", "ember", "contrast")
|
|
@@ -197,9 +197,11 @@ def read_descriptors(directory: Optional[Path] = None) -> List[Tuple[Path, Dict[
|
|
|
197
197
|
return found
|
|
198
198
|
for path in sorted(root.glob("*.json")):
|
|
199
199
|
try:
|
|
200
|
-
|
|
201
|
-
except (OSError, ValueError):
|
|
200
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
201
|
+
except (OSError, ValueError, RecursionError):
|
|
202
202
|
continue
|
|
203
|
+
if isinstance(value, dict): # a file that holds no object is no descriptor (FAA-200 (50))
|
|
204
|
+
found.append((path, value))
|
|
203
205
|
return found
|
|
204
206
|
|
|
205
207
|
|
|
@@ -222,10 +224,11 @@ def validate_descriptor(descriptor: Dict[str, Any]) -> List[str]:
|
|
|
222
224
|
return problems
|
|
223
225
|
if descriptor["protocol"] != PROTOCOL:
|
|
224
226
|
problems.append("protocol must be %s" % PROTOCOL)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
227
|
+
for key, pattern in (("id", _ID), ("instance", _INSTANCE)): # a string before its pattern (FAA-200 (9), (36))
|
|
228
|
+
if not isinstance(descriptor[key], str):
|
|
229
|
+
problems.append("%s must be a string" % key)
|
|
230
|
+
elif not pattern.match(descriptor[key]):
|
|
231
|
+
problems.append("%s must match %s" % (key, pattern.pattern))
|
|
229
232
|
if remote:
|
|
230
233
|
problems.extend(remote_origin_problems(descriptor["origin"]))
|
|
231
234
|
elif "origin" in descriptor and not _ORIGIN.match(str(descriptor["origin"])):
|
|
@@ -234,11 +237,11 @@ def validate_descriptor(descriptor: Dict[str, Any]) -> List[str]:
|
|
|
234
237
|
if "tokenFile" not in auth:
|
|
235
238
|
problems.append("auth.tokenFile is required")
|
|
236
239
|
elif not _LOCAL_PATH.match(str(auth["tokenFile"])):
|
|
237
|
-
problems.append("auth.tokenFile must be
|
|
240
|
+
problems.append("auth.tokenFile must be an absolute local path (/, ~/ or a drive letter), never a relative path, never a network share")
|
|
238
241
|
paths = descriptor.get("paths") or {}
|
|
239
242
|
for value in ([paths["data"]] if "data" in paths else []) + list(paths.get("logs") or []):
|
|
240
243
|
if not _LOCAL_PATH.match(str(value)):
|
|
241
|
-
problems.append("paths must be local paths, never a network share: %r" % value)
|
|
244
|
+
problems.append("paths must be absolute local paths (/, ~/ or a drive letter), never a relative path, never a network share: %r" % value)
|
|
242
245
|
header = auth.get("header", "Authorization")
|
|
243
246
|
scheme = auth.get("scheme", "Bearer")
|
|
244
247
|
if not (isinstance(header, str) and _AUTH_HEADER.fullmatch(header)):
|
|
@@ -405,7 +408,7 @@ def descriptor_member_problems(descriptor: Dict[str, Any]) -> List[str]:
|
|
|
405
408
|
elif "@" in repository and not _HIERARCHICAL.match(repository): # 0.62.0 readings 7: never an scp form's user
|
|
406
409
|
problems.append("source.repository with a user is a URL with an authority (https://user@host/path), never an scp form")
|
|
407
410
|
elif fabric_secrets.repository_credential(repository): # its name is no key (0.62.0 seam reading 6)
|
|
408
|
-
problems.append("FAC-SEM-086: /source/repository holds a value shaped like a credential; it names a repository")
|
|
411
|
+
problems.append("FAC-SEM-086: /source/repository holds a value shaped like a credential; it names a repository: write the repository's name in lower case if it is one")
|
|
409
412
|
if "peer" in descriptor:
|
|
410
413
|
peer = descriptor.get("peer")
|
|
411
414
|
if not isinstance(peer, dict):
|
|
@@ -589,8 +592,15 @@ def port_listener_refusal(port: int, reader: Optional[Any] = None) -> Optional[s
|
|
|
589
592
|
"FAA-225)." % (port, uid))
|
|
590
593
|
|
|
591
594
|
|
|
595
|
+
def warn_no_source(descriptor: Dict[str, Any], stacklevel: int = 2) -> None:
|
|
596
|
+
"""The rewrite warning of a descriptor that names no source.repository (FAA-198)."""
|
|
597
|
+
me = "%s.%s" % (descriptor["id"], descriptor["instance"])
|
|
598
|
+
warnings.warn("Descriptor %s %s; a new install is refused without it." % (me, NO_SOURCE_REPOSITORY),
|
|
599
|
+
NoSourceRepositoryWarning, stacklevel=stacklevel + 1)
|
|
600
|
+
|
|
601
|
+
|
|
592
602
|
def write_descriptor(descriptor: Dict[str, Any], directory: Optional[Path] = None, *, _stacklevel: int = 2,
|
|
593
|
-
port_owner: Optional[Any] = None) -> Path:
|
|
603
|
+
port_owner: Optional[Any] = None, _warned: bool = False) -> Path:
|
|
594
604
|
"""Installer-only. Refuses a port or id.instance another descriptor claims (a port is a claim), and a port another
|
|
595
605
|
OS account listens on (FAA-225; ``port_owner(port) -> (kind, uid) | None`` reads it, the kit's own by default).
|
|
596
606
|
|
|
@@ -604,9 +614,8 @@ def write_descriptor(descriptor: Dict[str, Any], directory: Optional[Path] = Non
|
|
|
604
614
|
refusal = source_refusal(descriptor, root)
|
|
605
615
|
if refusal:
|
|
606
616
|
raise ServiceError(refusal)
|
|
607
|
-
if not names_repository(descriptor):
|
|
608
|
-
|
|
609
|
-
NoSourceRepositoryWarning, stacklevel=_stacklevel)
|
|
617
|
+
if not names_repository(descriptor) and not _warned: # register_remote warns itself, before its token (FAA-200 (1))
|
|
618
|
+
warn_no_source(descriptor, _stacklevel)
|
|
610
619
|
# DEC-0019: only a local placement claims a port on this computer; DEC-0069: an app's descriptor names none.
|
|
611
620
|
port = None if placement_of(descriptor) == "remote" or "origin" not in descriptor else port_of(descriptor["origin"])
|
|
612
621
|
for path, other in read_descriptors(root):
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py
CHANGED
|
@@ -51,7 +51,9 @@ def make_event(event_id: Any, at: str, kind: str, level: str, text: str, *, subj
|
|
|
51
51
|
link: Optional[str] = None, notify: bool = False, trace_id: Optional[str] = None,
|
|
52
52
|
span_id: Optional[str] = None, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
53
53
|
"""One activity event: one sentence a person reads, never a machine id. `level` is one of `LEVELS`; `warn`, the
|
|
54
|
-
dashboard
|
|
54
|
+
dashboard kit's chip for "a person is needed" (`.fd-s-warn`), is stored as `warning`, the feed's name for the same
|
|
55
|
+
level, so a reader that draws chips shows a stored `warning` as the `warn` chip (FAA-276, FAA-277 (6)). `data`
|
|
56
|
+
(DEC-0070) adds machine-readable
|
|
55
57
|
fields beside the sentence, `{schema, value}`, never a secret: it is refused here, so no feed carries one.
|
|
56
58
|
|
|
57
59
|
An event about traced work carries its trace as a pair, `trace_id` and `span_id`
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py
CHANGED
|
@@ -21,7 +21,8 @@ says so three ways:
|
|
|
21
21
|
|
|
22
22
|
`HoldBook` keeps one JSON file (0600, written atomically); a ThreadingHTTPServer's handlers share it under the store
|
|
23
23
|
lock. It holds caller names, work labels, reasons and the operator's words, never a credential: `hold` refuses a text
|
|
24
|
-
the contract's detector reads as one.
|
|
24
|
+
the contract's detector reads as one. A record written since 0.83.0 carries `squash: "ecmascript"`; one without it
|
|
25
|
+
(0.81.0, 0.82.0) is matched under both squashes, since 0.81.0's Python twin squashed names with str.split() (FAA-320).
|
|
25
26
|
"""
|
|
26
27
|
from __future__ import annotations
|
|
27
28
|
|
|
@@ -65,6 +66,27 @@ def _squash(text: str) -> str:
|
|
|
65
66
|
return _SPACES.sub(" ", text).strip(" ")
|
|
66
67
|
|
|
67
68
|
|
|
69
|
+
#: What a record written since 0.83.0 carries: its names were squashed with ECMAScript's whitespace (FAA-320).
|
|
70
|
+
SQUASH = "ecmascript"
|
|
71
|
+
#: Python's str.isspace() class, the one 0.81.0's Python twin squashed a name with (`" ".join(text.split())`).
|
|
72
|
+
_PY_SPACES = re.compile("[\t\n\x0b\x0c\r\x1c-\x1f \x85\xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _legacy_squash(text: str) -> str:
|
|
76
|
+
"""A name as 0.81.0's Python twin wrote it (str.split()), written out so both twins read it alike."""
|
|
77
|
+
return _PY_SPACES.sub(" ", text).strip(" ")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _names(caller: Any, record: Dict[str, Any]) -> tuple:
|
|
81
|
+
"""The stored forms `caller` matches in `record`: the ECMAScript squash, and for a record with no `squash` mark
|
|
82
|
+
(written before 0.83.0) also the squash 0.81.0's Python twin wrote (FAA-320). None matches only None."""
|
|
83
|
+
if not isinstance(caller, str):
|
|
84
|
+
return (caller,)
|
|
85
|
+
if record.get("squash") == SQUASH:
|
|
86
|
+
return (_squash(caller),)
|
|
87
|
+
return (_squash(caller), _legacy_squash(caller))
|
|
88
|
+
|
|
89
|
+
|
|
68
90
|
def _instant(value: Any, name: str) -> str:
|
|
69
91
|
if utc_time(value) is None:
|
|
70
92
|
raise ServiceError("%s is a UTC instant (Z, at most 3 fraction digits)." % name)
|
|
@@ -197,6 +219,7 @@ class HoldBook:
|
|
|
197
219
|
job_id: Optional[str] = None, now: Optional[str] = None) -> Dict[str, Any]:
|
|
198
220
|
"""Hold `work` for `caller` by `reason`. Returns {hold, statusMessage, push}: `push` is the notify event when this
|
|
199
221
|
call opened the hold, None when the work joined an open one (one push per hold)."""
|
|
222
|
+
raw_caller = caller
|
|
200
223
|
caller, work = _text(caller, "caller"), _text(work, "work")
|
|
201
224
|
if reason not in HOLD_REASONS:
|
|
202
225
|
raise ServiceError("A hold's reason is one of %s." % ", ".join(HOLD_REASONS))
|
|
@@ -207,11 +230,11 @@ class HoldBook:
|
|
|
207
230
|
data = self._load()
|
|
208
231
|
self._expire(data, utc_time(at))
|
|
209
232
|
hold = next((h for h in data["holds"] if h.get("closed") is None and h["reason"] == reason
|
|
210
|
-
and h["caller"]
|
|
233
|
+
and h["caller"] in _names(raw_caller, h)), None)
|
|
211
234
|
opened = hold is None
|
|
212
235
|
if opened:
|
|
213
236
|
hold = {"id": uuid.uuid4().hex, "reason": reason, "caller": caller, "work": work, "since": at,
|
|
214
|
-
"until": until, "jobs": [], "closed": None}
|
|
237
|
+
"until": until, "jobs": [], "closed": None, "squash": SQUASH}
|
|
215
238
|
data["holds"].append(hold)
|
|
216
239
|
if job_id is not None and job_id not in hold["jobs"]:
|
|
217
240
|
hold["jobs"].append(str(job_id))
|
|
@@ -229,7 +252,7 @@ class HoldBook:
|
|
|
229
252
|
with self._lock:
|
|
230
253
|
data = self._load()
|
|
231
254
|
hold = next((h for h in data["holds"] if h.get("closed") is None and h["reason"] == reason
|
|
232
|
-
and h["caller"]
|
|
255
|
+
and h["caller"] in _names(caller, h)), None)
|
|
233
256
|
if hold is None:
|
|
234
257
|
return None
|
|
235
258
|
hold["closed"], hold["closedAt"] = "released", at
|
|
@@ -269,14 +292,15 @@ class HoldBook:
|
|
|
269
292
|
raise ServiceError("A raised limit needs its window's end (until): it falls back when the window ends.")
|
|
270
293
|
record: Dict[str, Any] = {"id": uuid.uuid4().hex, "reason": reason, "caller": None if caller is None else _text(caller, "caller"),
|
|
271
294
|
"job": None if job_id is None else str(job_id), "from": at, "until": until,
|
|
272
|
-
given[0]: size, "decision": dict(decision), "at": at}
|
|
295
|
+
given[0]: size, "decision": dict(decision), "at": at, "squash": SQUASH}
|
|
273
296
|
if given[0] != "limit":
|
|
274
297
|
record["left"] = size
|
|
275
298
|
with self._lock:
|
|
276
299
|
data = self._load()
|
|
277
300
|
self._expire(data, utc_time(at))
|
|
278
301
|
for hold in data["holds"]:
|
|
279
|
-
if hold.get("closed") is None and hold["reason"] == reason and
|
|
302
|
+
if hold.get("closed") is None and hold["reason"] == reason and (
|
|
303
|
+
caller is None or hold["caller"] in _names(caller, hold)) and (
|
|
280
304
|
job_id is None or str(job_id) in hold["jobs"]):
|
|
281
305
|
hold["closed"], hold["closedAt"], hold["lift"] = "lifted", at, record["id"]
|
|
282
306
|
data["lifts"].append(record)
|
|
@@ -291,7 +315,7 @@ class HoldBook:
|
|
|
291
315
|
def _matching(self, data: Dict[str, Any], reason: str, caller: Optional[str], job_id: Optional[str], at: int,
|
|
292
316
|
kind: str) -> List[Dict[str, Any]]:
|
|
293
317
|
return [l for l in data["lifts"] if l["reason"] == reason and kind in l and self._live(l, at)
|
|
294
|
-
and l.get("caller") in (
|
|
318
|
+
and (l.get("caller") is None or l["caller"] in _names(caller, l)) and l.get("job") in (None, job_id)]
|
|
295
319
|
|
|
296
320
|
def admit(self, *, reason: str, caller: str, job_id: Optional[str] = None, cost_usd: Optional[float] = None,
|
|
297
321
|
now: Optional[str] = None) -> Dict[str, Any]:
|