@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_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
|
|
|
@@ -55,6 +56,35 @@ LIFT_ACTION = "limit.lift"
|
|
|
55
56
|
_MAX_STATUS_MESSAGE = 500
|
|
56
57
|
_WAITING = re.compile(r"^waiting (daily-quota|caller-share|concurrency|money) since ([0-9][0-9TZ:.-]*?)(?:: ([\s\S]+))?\Z")
|
|
57
58
|
_SIZES = ("jobs", "usd", "limit")
|
|
59
|
+
#: One run of ECMAScript's whitespace, the class both twins squash with (FAA-316): Python's str.split() also takes
|
|
60
|
+
#: U+001C to U+001F and U+0085 and not U+FEFF, so the twins wrote different statusMessages.
|
|
61
|
+
_SPACES = re.compile("[%s]+" % fabric_secrets.JS_SPACE)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _squash(text: str) -> str:
|
|
65
|
+
"""Runs of ECMAScript whitespace as one space, none at either end (the Node twin's `squash`)."""
|
|
66
|
+
return _SPACES.sub(" ", text).strip(" ")
|
|
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))
|
|
58
88
|
|
|
59
89
|
|
|
60
90
|
def _instant(value: Any, name: str) -> str:
|
|
@@ -75,10 +105,10 @@ def waiting_message(reason: str, since: str, until: Optional[str] = None, messag
|
|
|
75
105
|
text = "waiting %s since %s" % (reason, since)
|
|
76
106
|
head = len(QUOTA_EXHAUSTED) + (0 if until is None else len(" until ") + len(until)) + len(": ")
|
|
77
107
|
if message is not None:
|
|
78
|
-
message =
|
|
108
|
+
message = _squash(str(message))
|
|
79
109
|
room = _MAX_STATUS_MESSAGE - head - len(text) - len(": ")
|
|
80
110
|
if message and room > 0:
|
|
81
|
-
text += ": " + (message if len(message) <= room else message[:room - 3].rstrip() + "...")
|
|
111
|
+
text += ": " + (message if len(message) <= room else message[:room - 3].rstrip(" ") + "...")
|
|
82
112
|
return pause_message(until, text)
|
|
83
113
|
|
|
84
114
|
|
|
@@ -124,11 +154,11 @@ def hold_sentence(caller: str, work: str, reason: str, until: Optional[str]) ->
|
|
|
124
154
|
|
|
125
155
|
|
|
126
156
|
def _text(value: Any, name: str, high: int = 200) -> str:
|
|
127
|
-
if not isinstance(value, str) or not value
|
|
157
|
+
if not isinstance(value, str) or not _squash(value) or len(value) > high:
|
|
128
158
|
raise ServiceError("%s is a text of 1 to %d characters." % (name, high))
|
|
129
159
|
if fabric_secrets.secret_value(value):
|
|
130
160
|
raise ServiceError("%s looks like a credential; a hold names callers and work, never a secret." % name)
|
|
131
|
-
return
|
|
161
|
+
return _squash(value)
|
|
132
162
|
|
|
133
163
|
|
|
134
164
|
class HoldBook:
|
|
@@ -189,6 +219,7 @@ class HoldBook:
|
|
|
189
219
|
job_id: Optional[str] = None, now: Optional[str] = None) -> Dict[str, Any]:
|
|
190
220
|
"""Hold `work` for `caller` by `reason`. Returns {hold, statusMessage, push}: `push` is the notify event when this
|
|
191
221
|
call opened the hold, None when the work joined an open one (one push per hold)."""
|
|
222
|
+
raw_caller = caller
|
|
192
223
|
caller, work = _text(caller, "caller"), _text(work, "work")
|
|
193
224
|
if reason not in HOLD_REASONS:
|
|
194
225
|
raise ServiceError("A hold's reason is one of %s." % ", ".join(HOLD_REASONS))
|
|
@@ -199,11 +230,11 @@ class HoldBook:
|
|
|
199
230
|
data = self._load()
|
|
200
231
|
self._expire(data, utc_time(at))
|
|
201
232
|
hold = next((h for h in data["holds"] if h.get("closed") is None and h["reason"] == reason
|
|
202
|
-
and h["caller"]
|
|
233
|
+
and h["caller"] in _names(raw_caller, h)), None)
|
|
203
234
|
opened = hold is None
|
|
204
235
|
if opened:
|
|
205
236
|
hold = {"id": uuid.uuid4().hex, "reason": reason, "caller": caller, "work": work, "since": at,
|
|
206
|
-
"until": until, "jobs": [], "closed": None}
|
|
237
|
+
"until": until, "jobs": [], "closed": None, "squash": SQUASH}
|
|
207
238
|
data["holds"].append(hold)
|
|
208
239
|
if job_id is not None and job_id not in hold["jobs"]:
|
|
209
240
|
hold["jobs"].append(str(job_id))
|
|
@@ -221,7 +252,7 @@ class HoldBook:
|
|
|
221
252
|
with self._lock:
|
|
222
253
|
data = self._load()
|
|
223
254
|
hold = next((h for h in data["holds"] if h.get("closed") is None and h["reason"] == reason
|
|
224
|
-
and h["caller"]
|
|
255
|
+
and h["caller"] in _names(caller, h)), None)
|
|
225
256
|
if hold is None:
|
|
226
257
|
return None
|
|
227
258
|
hold["closed"], hold["closedAt"] = "released", at
|
|
@@ -261,14 +292,15 @@ class HoldBook:
|
|
|
261
292
|
raise ServiceError("A raised limit needs its window's end (until): it falls back when the window ends.")
|
|
262
293
|
record: Dict[str, Any] = {"id": uuid.uuid4().hex, "reason": reason, "caller": None if caller is None else _text(caller, "caller"),
|
|
263
294
|
"job": None if job_id is None else str(job_id), "from": at, "until": until,
|
|
264
|
-
given[0]: size, "decision": dict(decision), "at": at}
|
|
295
|
+
given[0]: size, "decision": dict(decision), "at": at, "squash": SQUASH}
|
|
265
296
|
if given[0] != "limit":
|
|
266
297
|
record["left"] = size
|
|
267
298
|
with self._lock:
|
|
268
299
|
data = self._load()
|
|
269
300
|
self._expire(data, utc_time(at))
|
|
270
301
|
for hold in data["holds"]:
|
|
271
|
-
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 (
|
|
272
304
|
job_id is None or str(job_id) in hold["jobs"]):
|
|
273
305
|
hold["closed"], hold["closedAt"], hold["lift"] = "lifted", at, record["id"]
|
|
274
306
|
data["lifts"].append(record)
|
|
@@ -283,7 +315,7 @@ class HoldBook:
|
|
|
283
315
|
def _matching(self, data: Dict[str, Any], reason: str, caller: Optional[str], job_id: Optional[str], at: int,
|
|
284
316
|
kind: str) -> List[Dict[str, Any]]:
|
|
285
317
|
return [l for l in data["lifts"] if l["reason"] == reason and kind in l and self._live(l, at)
|
|
286
|
-
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)]
|
|
287
319
|
|
|
288
320
|
def admit(self, *, reason: str, caller: str, job_id: Optional[str] = None, cost_usd: Optional[float] = None,
|
|
289
321
|
now: Optional[str] = None) -> Dict[str, Any]:
|
|
@@ -50,6 +50,8 @@ from pathlib import Path as _Path # noqa: E402
|
|
|
50
50
|
if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from any working directory (FAA-65)
|
|
51
51
|
_sys.path.insert(0, str(_Path(__file__).resolve().parent))
|
|
52
52
|
from fabric_jsvalue import json_stringify # noqa: E402 - beside this file
|
|
53
|
+
import fabric_secrets # noqa: E402 - beside this file: the WHATWG host reading (FAA-98, FAA-309)
|
|
54
|
+
import fabric_socket_proof # noqa: E402 - beside this file: the one loopback host rule (FAA-187 (15))
|
|
53
55
|
import fabric_peer_owner as _po # noqa: E402 - beside this file: the listener's owner (DEC-0154 HA-2)
|
|
54
56
|
|
|
55
57
|
RETRY_STATUSES = (408, 425, 429, 500, 502, 503, 504)
|
|
@@ -73,8 +75,9 @@ class HttpError(Exception):
|
|
|
73
75
|
self.retry_after_s = retry_after_s
|
|
74
76
|
self.body = bytes(body[:2048])
|
|
75
77
|
self.kind = kind # http (an answer), connection, timeout, too-large, refused, foreign-user
|
|
76
|
-
#: For foreign-user, why (FAA-272): `table-restricted` (this process sees no other process's sockets,
|
|
77
|
-
#: never clears), the owner's kind (`another-account`,
|
|
78
|
+
#: For foreign-user, why (FAA-272): `table-restricted` (this process sees no other process's sockets, or on
|
|
79
|
+
#: macOS no row for this connection, DEC-0155 RT-1; it never clears), the owner's kind (`another-account`,
|
|
80
|
+
#: `system`) or the check's reason (`no-row`, from Linux and Windows only).
|
|
78
81
|
self.reason = reason
|
|
79
82
|
|
|
80
83
|
|
|
@@ -210,28 +213,28 @@ def redact_url(url: str) -> str:
|
|
|
210
213
|
return urllib.parse.urlunsplit((parts.scheme.lower(), netloc, path, "", ""))
|
|
211
214
|
|
|
212
215
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
216
|
+
#: Loopback as a WHATWG URL parser leaves the host: the kit's one rule, shared with fabric_delegation (FAA-187 (15)).
|
|
217
|
+
_is_loopback = fabric_socket_proof.loopback_host
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _idna_sendable(host: Optional[str]) -> bool:
|
|
221
|
+
"""Whether http.client can write the host: a non-ASCII one goes out in IDNA through Python's own codec, which refuses
|
|
222
|
+
some hosts `new URL` maps (U+180E, a fullwidth dot beside a dot), so such a URL would fail on every call (FAA-187
|
|
223
|
+
(16)); it is refused at the check instead."""
|
|
224
|
+
if not host or host.isascii():
|
|
217
225
|
return True
|
|
218
|
-
if not host or any(c.isspace() for c in host):
|
|
219
|
-
return False
|
|
220
226
|
try:
|
|
221
|
-
|
|
222
|
-
return
|
|
223
|
-
except
|
|
224
|
-
pass
|
|
225
|
-
host = host[:-1] if host.endswith(".") else host
|
|
226
|
-
try: # the short IPv4 forms a WHATWG URL also reads (127.1, 2130706433, 0x7f.1)
|
|
227
|
-
return socket.inet_aton(host)[0] == 127
|
|
228
|
-
except (OSError, ValueError):
|
|
227
|
+
host.encode("idna")
|
|
228
|
+
return True
|
|
229
|
+
except UnicodeError:
|
|
229
230
|
return False
|
|
230
231
|
|
|
231
232
|
|
|
232
233
|
def check_url(url: str, *, insecure: bool = False) -> None:
|
|
233
234
|
"""Refuse what this client never sends: a scheme other than http or https, no host, credentials in the URL, or
|
|
234
|
-
plain http to a host that is not loopback (unless `insecure`).
|
|
235
|
+
plain http to a host that is not loopback (unless `insecure`). A host `new URL` refuses for its structure (a last
|
|
236
|
+
label that is a number but no IPv4 address, `a127.0.0.1`, `09`; a host UTS46 refuses, a joiner beside ASCII alone) is
|
|
237
|
+
refused as the Node twin refuses it, read by fabric_secrets.url_refused (FAA-187 (14))."""
|
|
235
238
|
try:
|
|
236
239
|
# The ends are trimmed as urllib trims what it sends (and `new URL` too); a URL ending in a line break, which
|
|
237
240
|
# 0.55.0 sent, still passes (0.56.0 product r17).
|
|
@@ -241,7 +244,7 @@ def check_url(url: str, *, insecure: bool = False) -> None:
|
|
|
241
244
|
parts = urllib.parse.urlsplit(sent)
|
|
242
245
|
host = parts.hostname
|
|
243
246
|
parts.port # noqa: B018 - raises on a bad port
|
|
244
|
-
if not _authority_ok(parts.netloc):
|
|
247
|
+
if not _authority_ok(parts.netloc) or fabric_secrets.url_refused(sent) or not _idna_sendable(host):
|
|
245
248
|
raise ValueError("authority")
|
|
246
249
|
except (ValueError, TypeError):
|
|
247
250
|
raise HttpError("the URL does not parse", url=redact_url(url), kind="refused")
|
|
@@ -70,8 +70,9 @@ class HubError(Exception):
|
|
|
70
70
|
def listener_refusal(exc: Any) -> HubError:
|
|
71
71
|
"""The HubError for a hub port the peer-owner check refused (DEC-0154 HA-2): code `foreign-user`, the pinned
|
|
72
72
|
contract's state name, with the owner's reason in the sentence and in `data.reason` (FAA-272): `table-restricted`
|
|
73
|
-
when this process's socket table hides other processes' sockets,
|
|
74
|
-
otherwise the owner's kind or the check's
|
|
73
|
+
when this process's socket table hides other processes' sockets, or, on macOS, holds no row for this connection
|
|
74
|
+
(DEC-0155 RT-1, FAA-312), which never clears, so it is not retryable; otherwise the owner's kind or the check's
|
|
75
|
+
reason (`another-account`, or `no-row`, which only Linux and Windows give), retryable."""
|
|
75
76
|
owner = getattr(exc, "owner", None)
|
|
76
77
|
why = (getattr(owner, "reason", None) or getattr(owner, "kind", None) or getattr(exc, "code", None) or "unknown")
|
|
77
78
|
reason = _po.TABLE_RESTRICTED if str(why).startswith(_po.TABLE_RESTRICTED) else str(why).split(":", 1)[0]
|
|
@@ -94,25 +95,41 @@ def fabric_root(platform: str = sys.platform, env: Optional[Dict[str, str]] = No
|
|
|
94
95
|
return Path(env.get("XDG_DATA_HOME") or str(home / ".local/share")) / "passioncode-fabric"
|
|
95
96
|
|
|
96
97
|
|
|
97
|
-
def
|
|
98
|
-
"""
|
|
98
|
+
def _windows_pid_alive(pid: int, kernel: Any = None, last_error: Any = None) -> Optional[bool]:
|
|
99
|
+
"""pid_alive on Windows, where os.kill(pid, 0) would send CTRL_C_EVENT: the check Node's process.kill(pid, 0) makes
|
|
100
|
+
(libuv's uv_kill), asking OpenProcess for the same access (terminate, query, synchronize; never the limited query an
|
|
101
|
+
elevated process of the same user grants, FAA-323). Refused with ERROR_ACCESS_DENIED (5), another account's or an
|
|
102
|
+
elevated process: None, as Node answers EPERM there (FAA-304); refused otherwise, no process; opened, running
|
|
103
|
+
while its exit code is STILL_ACTIVE and a zero wait times out. `kernel` and `last_error` stand in for kernel32 and
|
|
104
|
+
ctypes.get_last_error in a test."""
|
|
105
|
+
import ctypes
|
|
106
|
+
if kernel is None:
|
|
107
|
+
kernel = ctypes.WinDLL("kernel32", use_last_error=True) # type: ignore[attr-defined]
|
|
108
|
+
last_error = ctypes.get_last_error # type: ignore[attr-defined]
|
|
109
|
+
handle = kernel.OpenProcess(0x0001 | 0x0400 | 0x00100000, False, pid) # TERMINATE | QUERY_INFORMATION | SYNCHRONIZE
|
|
110
|
+
if not handle:
|
|
111
|
+
return None if last_error() == 5 else False # ERROR_ACCESS_DENIED: not verified, never called running
|
|
112
|
+
try:
|
|
113
|
+
code = ctypes.c_ulong()
|
|
114
|
+
if not kernel.GetExitCodeProcess(handle, ctypes.byref(code)) or code.value != 259: # STILL_ACTIVE
|
|
115
|
+
return False
|
|
116
|
+
return kernel.WaitForSingleObject(handle, 0) != 0 # WAIT_OBJECT_0: ended, with exit code 259
|
|
117
|
+
finally:
|
|
118
|
+
kernel.CloseHandle(handle)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def pid_alive(pid: int) -> Optional[bool]:
|
|
122
|
+
"""True: a process of this OS account runs under `pid`; False: none does. None (falsy): a process of another account
|
|
123
|
+
does (EPERM; on Windows, OpenProcess refused with access denied), as when the id hub.json names was reused: not
|
|
124
|
+
verified, never called running (FAA-304, the session hooks' answer since FAA-254). Never signals it."""
|
|
99
125
|
if pid <= 0:
|
|
100
126
|
return False
|
|
101
127
|
if sys.platform == "win32":
|
|
102
|
-
|
|
103
|
-
kernel = ctypes.windll.kernel32 # type: ignore[attr-defined]
|
|
104
|
-
handle = kernel.OpenProcess(0x1000, False, pid) # PROCESS_QUERY_LIMITED_INFORMATION
|
|
105
|
-
if not handle:
|
|
106
|
-
return False
|
|
107
|
-
try:
|
|
108
|
-
code = ctypes.c_ulong()
|
|
109
|
-
return bool(kernel.GetExitCodeProcess(handle, ctypes.byref(code))) and code.value == 259 # STILL_ACTIVE
|
|
110
|
-
finally:
|
|
111
|
-
kernel.CloseHandle(handle)
|
|
128
|
+
return _windows_pid_alive(pid)
|
|
112
129
|
try:
|
|
113
130
|
os.kill(pid, 0)
|
|
114
131
|
except PermissionError:
|
|
115
|
-
return
|
|
132
|
+
return None # another account's process under a reused id: not verified, and not this account's Fabric
|
|
116
133
|
except OSError:
|
|
117
134
|
return False
|
|
118
135
|
return True
|
|
@@ -142,7 +159,8 @@ def _read_small(path: Path) -> Optional[str]:
|
|
|
142
159
|
def locate_hub(root: Optional[Path] = None, alive: Callable[[int], bool] = pid_alive, estate: Optional[Dict[str, Any]] = None,
|
|
143
160
|
now: Optional[float] = None) -> Dict[str, Any]:
|
|
144
161
|
"""`hub.json` read the way an agent must, every call. Answers `{origin, mcpUrl, doorTokenFile, pid, startedAt,
|
|
145
|
-
features}` or raises `HubError` (`not-running`, retryable; `not-a-hub
|
|
162
|
+
features}` or raises `HubError` (`not-running`, retryable; `not-a-hub`, retryable when hub.json names a process of
|
|
163
|
+
another OS account, which is not verified, FAA-304). Reads no token. `estate` is
|
|
146
164
|
`{"record": <estate record>, "applied": {"revision", "digest"?} | None}`: with it, a `hubKey` the document names is
|
|
147
165
|
checked by `hub_key_certified` (DEC-0060); fabric-hub/0.1 names none, and then nothing is checked."""
|
|
148
166
|
root = Path(root) if root is not None else fabric_root()
|
|
@@ -174,7 +192,11 @@ def locate_hub(root: Optional[Path] = None, alive: Callable[[int], bool] = pid_a
|
|
|
174
192
|
token_file = doc.get("doorTokenFile") if isinstance(doc.get("doorTokenFile"), str) else ""
|
|
175
193
|
if not os.path.isabs(token_file) or os.path.realpath(os.path.dirname(token_file)) != os.path.realpath(str(root)):
|
|
176
194
|
raise not_hub("names a door token outside the registry root")
|
|
177
|
-
|
|
195
|
+
live = alive(pid)
|
|
196
|
+
if live is None: # FAA-304: another account's process under that id, so not this account's Fabric (as FAA-254)
|
|
197
|
+
raise HubError("not-a-hub", "Fabric's hub.json names process %d, which runs under another OS account: not "
|
|
198
|
+
"verified." % pid, True)
|
|
199
|
+
if not live:
|
|
178
200
|
raise HubError("not-running", "Fabric is not running: hub.json names process %d, which is not running." % pid, True)
|
|
179
201
|
features = doc.get("features")
|
|
180
202
|
features = [f for f in features if isinstance(f, str)] if isinstance(features, list) else []
|