@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_socket_proof.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""fabric_socket_proof: this OS account's own TCP sockets, read from its own processes (layer L0; DEC-0155 RT-2,
|
|
2
|
+
FAA-292; FAA-272).
|
|
3
|
+
|
|
4
|
+
macOS 27.2 shows an ad-hoc-signed interpreter (a Homebrew or uv CPython) only its own sockets in the host's socket
|
|
5
|
+
table, so `fabric_peer_owner` reads such a table as `table-restricted` and cannot tell who accepted a connection. The
|
|
6
|
+
question can still be asked of this account's own processes, which the restriction leaves readable: whether one of them
|
|
7
|
+
holds the very socket that accepted the connection (RT-2, the accepted-socket proof), or a listener bound exactly to
|
|
8
|
+
127.0.0.1:<port> (HA-2's macOS row, FAA-272). Another account's process answers EPERM and is skipped, so a proof only
|
|
9
|
+
ever says "this account's" or "not proved"; never another account's, the system's or nobody's.
|
|
10
|
+
|
|
11
|
+
accepted(server, client, wait=...) -> Proof the socket at `server` connected to `client` is in one of this
|
|
12
|
+
account's processes (`wait`: read again for ACCEPT_WAIT_SECONDS
|
|
13
|
+
until the server has called accept())
|
|
14
|
+
own_listener(port) -> Proof a listener of this account on exactly 127.0.0.1:<port>
|
|
15
|
+
|
|
16
|
+
Two readers, chosen by `method()`:
|
|
17
|
+
|
|
18
|
+
- `libproc` (Python's): `proc_listpids(PROC_UID_ONLY)`, `proc_pidinfo(PROC_PIDLISTFDS)` and
|
|
19
|
+
`proc_pidfdinfo(PROC_PIDFDSOCKETINFO)` through ctypes, the records `lsof` itself reads, without a subprocess:
|
|
20
|
+
Project Observatory's `peer_owner` 1.3.1 reader (sha256 `b3831e5f...`, FAA-292). The `socket_fdinfo` layout is not
|
|
21
|
+
an API, so the first read in a process calibrates it on a connected pair of its own, as the sysctl reader does; a
|
|
22
|
+
layout that fails is never used again in this process, and a calibration that could not read at all is asked again.
|
|
23
|
+
- `lsof` (the Node twin's, and Python's when libproc is unavailable or uncalibrated): `lsof -nP -a -u <uid>
|
|
24
|
+
-iTCP:<port> -sTCP:<state> -F pun`, the exact address pair and the numeric uid. The twins agree on answers, not on
|
|
25
|
+
the program they run (DEC-0155 Compatibility); `test/fixtures/peer-owner/socket-proof-cases.json` holds both.
|
|
26
|
+
|
|
27
|
+
The proof is weaker than a table's `so_uid`: "a process of this uid holds the descriptor", not "the socket's
|
|
28
|
+
credential is this uid". A socket another account accepted and passed with SCM_RIGHTS to a process of this uid would
|
|
29
|
+
read as this account's; a caller that knows the server's pid closes that with `expect_pid`. One socket several of this
|
|
30
|
+
account's processes hold (an inherited descriptor) is proved with pid None, reason `several-processes`. A process whose
|
|
31
|
+
real and effective uids differ is refused (`setuid-context`): XNU lists and checks processes by the effective uid.
|
|
32
|
+
Reasons, as DEC-0155 RT-1 spells them: `table-restricted` (read, and no socket of this account matches) or
|
|
33
|
+
`table-restricted: <detail>` (the proof could not be read). Bounded by MAX_PIDS, MAX_FDS and TIMEOUT_SECONDS; nothing
|
|
34
|
+
is cached across questions but the calibration verdict. Standard library only, no other kit module. Tests:
|
|
35
|
+
`test/test_fabric_socket_proof.py`; the Node twin is `fabric-socket-proof.mjs`.
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import ipaddress
|
|
40
|
+
import os
|
|
41
|
+
import socket
|
|
42
|
+
import struct
|
|
43
|
+
import subprocess
|
|
44
|
+
import sys
|
|
45
|
+
import threading
|
|
46
|
+
import time
|
|
47
|
+
from typing import Callable, List, NamedTuple, Optional, Tuple
|
|
48
|
+
|
|
49
|
+
TABLE_RESTRICTED = "table-restricted"
|
|
50
|
+
LIBPROC = "libproc"
|
|
51
|
+
LSOF_METHOD = "lsof"
|
|
52
|
+
LSOF = "/usr/sbin/lsof"
|
|
53
|
+
#: Every read of one question is over within this many seconds (the accept wait included).
|
|
54
|
+
TIMEOUT_SECONDS = 2.0
|
|
55
|
+
#: How long `accepted(..., wait=True)` reads again for a server that has not called accept() yet.
|
|
56
|
+
ACCEPT_WAIT_SECONDS = 0.5
|
|
57
|
+
MAX_PIDS = 16384
|
|
58
|
+
MAX_FDS = 65536
|
|
59
|
+
MAX_OUTPUT_BYTES = 4 * 1024 * 1024
|
|
60
|
+
|
|
61
|
+
#: macOS `sys/proc_info.h`: selectors and the `struct socket_fdinfo` offsets read here. The struct is 792 bytes:
|
|
62
|
+
#: `proc_fileinfo` (24), then `socket_info`, whose `soi_so` is the kernel's socket (the same in every process that
|
|
63
|
+
#: holds it) and whose `soi_proto.pri_tcp` starts with an `in_sockinfo` (ports as ints holding a network-order port,
|
|
64
|
+
#: `insi_vflag`, the foreign and local `in4in6_addr`) and carries `tcpsi_state`.
|
|
65
|
+
PROC_UID_ONLY = 4
|
|
66
|
+
PROC_PIDLISTFDS = 1
|
|
67
|
+
PROX_FDTYPE_SOCKET = 2
|
|
68
|
+
PROC_PIDFDSOCKETINFO = 3
|
|
69
|
+
SOCKINFO_TCP = 2
|
|
70
|
+
SOCKET_FDINFO_SIZE = 792
|
|
71
|
+
_SOI = 24 # socket_fdinfo.psi
|
|
72
|
+
_SOI_SO = _SOI + 136 # socket_info.soi_so
|
|
73
|
+
_SOI_KIND = _SOI + 232 # socket_info.soi_kind
|
|
74
|
+
_INI = _SOI + 240 # soi_proto.pri_tcp.tcpsi_ini
|
|
75
|
+
_INI_VFLAG = _INI + 24 # INI_IPV4 0x1, INI_IPV6 0x2
|
|
76
|
+
_INI_FOREIGN = _INI + 32
|
|
77
|
+
_INI_LOCAL = _INI + 48
|
|
78
|
+
_TCPSI_STATE = _INI + 80 # TSI_S_CLOSED 0, TSI_S_LISTEN 1, TSI_S_TIME_WAIT 10
|
|
79
|
+
_TSI_S_CLOSED = 0
|
|
80
|
+
_TSI_S_LISTEN = 1
|
|
81
|
+
_TSI_S_TIME_WAIT = 10
|
|
82
|
+
|
|
83
|
+
Address = Tuple[str, int]
|
|
84
|
+
# Real types, not postponed strings (as fabric_peer_owner's Owner): a NamedTuple's generated __new__ resolves its
|
|
85
|
+
# annotations in a namespace of its own, where `Address` is no name.
|
|
86
|
+
_SocketFields = NamedTuple("_SocketFields", [("local", Tuple[str, int]), ("remote", Tuple[str, int]),
|
|
87
|
+
("listening", bool), ("pid", Optional[int])])
|
|
88
|
+
_ProofFields = NamedTuple("_ProofFields", [("proved", bool), ("pid", Optional[int]), ("method", str),
|
|
89
|
+
("reason", Optional[str])])
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Socket(_SocketFields):
|
|
93
|
+
"""One TCP socket of this account: `pid` is None when several of its processes hold it."""
|
|
94
|
+
__slots__ = ()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Proof(_ProofFields):
|
|
98
|
+
"""Whether this account's process holds the socket asked about; `reason` says why not, or `several-processes`."""
|
|
99
|
+
__slots__ = ()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ProofUnreadable(Exception):
|
|
103
|
+
"""This account's sockets could not be read; the message is the reason, never a socket's content."""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _host(host: str) -> str:
|
|
107
|
+
"""One spelling per address, as fabric_peer_owner.norm_host (the same rule, kept here so this module stands alone):
|
|
108
|
+
an IPv4-mapped IPv6 address is its IPv4 form, an unspecified one `*`, `localhost` 127.0.0.1, and `[...]` loses its
|
|
109
|
+
brackets. Anything unparseable is returned unchanged (and matches only itself)."""
|
|
110
|
+
if host.startswith("[") and host.endswith("]"):
|
|
111
|
+
host = host[1:-1]
|
|
112
|
+
if host in ("*", ""):
|
|
113
|
+
return "*"
|
|
114
|
+
if host == "localhost":
|
|
115
|
+
return "127.0.0.1"
|
|
116
|
+
host = host.split("%", 1)[0]
|
|
117
|
+
try:
|
|
118
|
+
ip = ipaddress.ip_address(host)
|
|
119
|
+
except ValueError:
|
|
120
|
+
return host
|
|
121
|
+
if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped is not None:
|
|
122
|
+
ip = ip.ipv4_mapped
|
|
123
|
+
return "*" if ip.is_unspecified else str(ip)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _digits(text: str) -> bool:
|
|
127
|
+
"""`[0-9]+`, as the Node twin reads a number (`str.isdigit` also takes `٤`)."""
|
|
128
|
+
return text != "" and text.isascii() and text.isdigit()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def loopback_host(host: str) -> bool:
|
|
132
|
+
"""Whether a URL's host, as a URL parser leaves it (urlsplit's `hostname`, lower case, no brackets), names this
|
|
133
|
+
computer's loopback the way a WHATWG parser reads it: `localhost` or a name under `.localhost` (which resolves to
|
|
134
|
+
loopback), ::1, or an IPv4 address in 127/8 in any form `new URL` reads (`127.1`, `2130706433`, `0x7f.1`, one
|
|
135
|
+
trailing dot). Never an IPv4-mapped address (Node writes it in hex and refuses it), one with a zone, or whitespace.
|
|
136
|
+
The one rule fabric_http's check_url and fabric_delegation's key set source read (FAA-187 (15)); the Node twin's
|
|
137
|
+
`loopbackHost` reads `new URL`'s hostname."""
|
|
138
|
+
host = (host or "").lower()
|
|
139
|
+
if host == "localhost" or host.endswith(".localhost"):
|
|
140
|
+
return True
|
|
141
|
+
if not host or any(c.isspace() for c in host) or "%" in host:
|
|
142
|
+
return False
|
|
143
|
+
try:
|
|
144
|
+
address = ipaddress.ip_address(host)
|
|
145
|
+
return address.is_loopback and not getattr(address, "ipv4_mapped", None)
|
|
146
|
+
except ValueError:
|
|
147
|
+
pass
|
|
148
|
+
host = host[:-1] if host.endswith(".") else host
|
|
149
|
+
try: # the short IPv4 forms a WHATWG URL also reads (127.1, 2130706433, 0x7f.1)
|
|
150
|
+
return socket.inet_aton(host)[0] == 127
|
|
151
|
+
except (OSError, ValueError):
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# -- parsers: pure functions over captured records and text (test/fixtures/peer-owner/socket-proof-cases.json) -------
|
|
156
|
+
|
|
157
|
+
def parse_socket_fdinfo(blob: bytes) -> Optional[Tuple[Address, Address, int, int]]:
|
|
158
|
+
"""One `proc_pidfdinfo(PROC_PIDFDSOCKETINFO)` record: (local, remote, TCP state, socket handle) for a TCP socket,
|
|
159
|
+
None for any other kind. A short record is unreadable."""
|
|
160
|
+
if len(blob) < SOCKET_FDINFO_SIZE:
|
|
161
|
+
raise ProofUnreadable("libproc: short socket record")
|
|
162
|
+
if struct.unpack_from("<i", blob, _SOI_KIND)[0] != SOCKINFO_TCP:
|
|
163
|
+
return None
|
|
164
|
+
fport, lport = struct.unpack_from(">H", blob, _INI)[0], struct.unpack_from(">H", blob, _INI + 4)[0]
|
|
165
|
+
family, at = (socket.AF_INET, 12) if blob[_INI_VFLAG] & 0x1 else (socket.AF_INET6, 0)
|
|
166
|
+
size = 4 if family == socket.AF_INET else 16
|
|
167
|
+
foreign = socket.inet_ntop(family, bytes(blob[_INI_FOREIGN + at:_INI_FOREIGN + at + size]))
|
|
168
|
+
local = socket.inet_ntop(family, bytes(blob[_INI_LOCAL + at:_INI_LOCAL + at + size]))
|
|
169
|
+
state = struct.unpack_from("<i", blob, _TCPSI_STATE)[0]
|
|
170
|
+
return (_host(local), lport), (_host(foreign), fport), state, struct.unpack_from("<Q", blob, _SOI_SO)[0]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def own_sockets(records: List[Tuple[int, Address, Address, int, int]]) -> List[Socket]:
|
|
174
|
+
"""Sockets from this account's records (pid, local, remote, state, handle): one per handle, pid None when several
|
|
175
|
+
processes hold it. Closed and TIME_WAIT sockets are left out."""
|
|
176
|
+
by_handle: dict = {}
|
|
177
|
+
for pid, local, remote, state, handle in records:
|
|
178
|
+
if state in (_TSI_S_CLOSED, _TSI_S_TIME_WAIT):
|
|
179
|
+
continue
|
|
180
|
+
by_handle.setdefault(handle, (local, remote, state, []))[3].append(pid)
|
|
181
|
+
return [Socket(local, remote, state == _TSI_S_LISTEN, pids[0] if len(set(pids)) == 1 else None)
|
|
182
|
+
for local, remote, state, pids in by_handle.values()]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _lsof_address(text: str) -> Optional[Address]:
|
|
186
|
+
"""`127.0.0.1:47311`, `[::1]:47070` or `*:47312`: the port after the last colon, ASCII digits only."""
|
|
187
|
+
host, sep, port = text.rpartition(":")
|
|
188
|
+
if not sep or not _digits(port) or int(port) > 65535:
|
|
189
|
+
return None
|
|
190
|
+
return _host(host), int(port)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def parse_lsof_sockets(text: str, uid: int) -> List[Socket]:
|
|
194
|
+
"""`lsof -nP -a -u <uid> -iTCP:<port> -sTCP:<state> -F pun`: the `n` lines of every process block whose `u` line is
|
|
195
|
+
exactly `uid` (a block of another uid, or with no uid, is not read). A name with `->` is a connection, one without a
|
|
196
|
+
listener. The same socket listed by several processes (one address pair: TCP holds one connection per pair) is one
|
|
197
|
+
socket with pid None."""
|
|
198
|
+
found: dict = {}
|
|
199
|
+
pid = owner = None
|
|
200
|
+
for line in text.splitlines():
|
|
201
|
+
tag, value = line[:1], line[1:]
|
|
202
|
+
if tag == "p":
|
|
203
|
+
pid, owner = (int(value) if _digits(value) else None), None
|
|
204
|
+
elif tag == "u":
|
|
205
|
+
owner = int(value) if _digits(value) else None
|
|
206
|
+
elif tag == "n" and pid is not None and owner == uid:
|
|
207
|
+
local_text, arrow, remote_text = value.partition("->")
|
|
208
|
+
local = _lsof_address(local_text)
|
|
209
|
+
remote = _lsof_address(remote_text) if arrow else ("*", 0)
|
|
210
|
+
if local is None or remote is None:
|
|
211
|
+
continue
|
|
212
|
+
key = (local, remote, not arrow)
|
|
213
|
+
found.setdefault(key, []).append(pid)
|
|
214
|
+
return [Socket(local, remote, listening, pids[0] if len(set(pids)) == 1 else None)
|
|
215
|
+
for (local, remote, listening), pids in found.items()]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _proved(hits: List[Socket], how: str) -> Proof:
|
|
219
|
+
if not hits:
|
|
220
|
+
return Proof(False, None, how, TABLE_RESTRICTED)
|
|
221
|
+
pids = {s.pid for s in hits}
|
|
222
|
+
if len(pids) == 1 and None not in pids:
|
|
223
|
+
return Proof(True, pids.pop(), how, None)
|
|
224
|
+
return Proof(True, None, how, "several-processes")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def accepted_in(sockets: List[Socket], server: Address, client: Address, how: str) -> Proof:
|
|
228
|
+
"""RT-2 over this account's sockets: the one at `server` connected to `client`. Two different sockets with that
|
|
229
|
+
address pair cannot be told apart (`several-rows`)."""
|
|
230
|
+
hits = [s for s in sockets if not s.listening and s.local == server and s.remote == client]
|
|
231
|
+
if len(hits) > 1:
|
|
232
|
+
return Proof(False, None, how, "several-rows")
|
|
233
|
+
return _proved(hits, how)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def listener_in(sockets: List[Socket], port: int, how: str) -> Proof:
|
|
237
|
+
"""FAA-272 over this account's sockets: a listener bound exactly to 127.0.0.1:<port>. XNU refuses another ordinary
|
|
238
|
+
account that bind beside ours (`in_pcbbind`); our wildcard or ::1 listener, and root's, cannot be told from a
|
|
239
|
+
hidden one, so they prove nothing (DEC-0155 RT-4)."""
|
|
240
|
+
return _proved([s for s in sockets if s.listening and s.local == ("127.0.0.1", port)], how)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# -- readers ----------------------------------------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
_LOCK = threading.Lock()
|
|
246
|
+
#: None until a calibration of the libproc layout reaches a verdict in this process, then True or False.
|
|
247
|
+
_LIBPROC_OK: list = [None]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _run(argv: List[str]) -> str:
|
|
251
|
+
"""One bounded run of lsof; its exit 1 with nothing printed is "no socket matched", not a failure."""
|
|
252
|
+
try:
|
|
253
|
+
done = subprocess.run(argv, capture_output=True, timeout=TIMEOUT_SECONDS, check=False, stdin=subprocess.DEVNULL)
|
|
254
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
255
|
+
raise ProofUnreadable("lsof: %s" % type(exc).__name__) from None
|
|
256
|
+
if done.returncode == 1 and not done.stdout.strip():
|
|
257
|
+
return ""
|
|
258
|
+
if done.returncode != 0:
|
|
259
|
+
raise ProofUnreadable("lsof: exit %d" % done.returncode)
|
|
260
|
+
if len(done.stdout) > MAX_OUTPUT_BYTES:
|
|
261
|
+
raise ProofUnreadable("lsof: output over %d bytes" % MAX_OUTPUT_BYTES)
|
|
262
|
+
return done.stdout.decode("utf-8", "replace")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _libproc_records(uid: int, deadline: float) -> List[Tuple[int, Address, Address, int, int]]:
|
|
266
|
+
"""This uid's processes' TCP sockets, as (pid, local, remote, state, handle). A process that cannot be read (gone,
|
|
267
|
+
or EPERM) is skipped: it can only lose an answer, never make one."""
|
|
268
|
+
try:
|
|
269
|
+
import ctypes
|
|
270
|
+
lib = ctypes.CDLL("/usr/lib/libproc.dylib", use_errno=True)
|
|
271
|
+
lib.proc_listpids.argtypes = [ctypes.c_uint32, ctypes.c_uint32, ctypes.c_void_p, ctypes.c_int]
|
|
272
|
+
lib.proc_pidinfo.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_uint64, ctypes.c_void_p, ctypes.c_int]
|
|
273
|
+
lib.proc_pidfdinfo.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.c_int]
|
|
274
|
+
except (ImportError, OSError, TypeError, AttributeError) as exc:
|
|
275
|
+
raise ProofUnreadable("libproc: %s" % type(exc).__name__) from None
|
|
276
|
+
for fn in (lib.proc_listpids, lib.proc_pidinfo, lib.proc_pidfdinfo):
|
|
277
|
+
fn.restype = ctypes.c_int
|
|
278
|
+
need = lib.proc_listpids(PROC_UID_ONLY, uid, None, 0)
|
|
279
|
+
if need <= 0:
|
|
280
|
+
raise ProofUnreadable("libproc: errno %d" % ctypes.get_errno())
|
|
281
|
+
pids = (ctypes.c_int * min(need // 4 + 64, MAX_PIDS))()
|
|
282
|
+
got = lib.proc_listpids(PROC_UID_ONLY, uid, pids, ctypes.sizeof(pids))
|
|
283
|
+
if got <= 0:
|
|
284
|
+
raise ProofUnreadable("libproc: errno %d" % ctypes.get_errno())
|
|
285
|
+
if got // 4 >= MAX_PIDS:
|
|
286
|
+
raise ProofUnreadable("libproc: over %d processes" % MAX_PIDS)
|
|
287
|
+
record = ctypes.create_string_buffer(SOCKET_FDINFO_SIZE)
|
|
288
|
+
out = []
|
|
289
|
+
for pid in pids[:got // 4]:
|
|
290
|
+
if pid <= 0:
|
|
291
|
+
continue
|
|
292
|
+
if time.monotonic() > deadline:
|
|
293
|
+
raise ProofUnreadable("libproc: deadline")
|
|
294
|
+
size = lib.proc_pidinfo(pid, PROC_PIDLISTFDS, 0, None, 0)
|
|
295
|
+
if size <= 0:
|
|
296
|
+
continue
|
|
297
|
+
fds = ctypes.create_string_buffer(min(size + 32 * 8, MAX_FDS * 8))
|
|
298
|
+
size = lib.proc_pidinfo(pid, PROC_PIDLISTFDS, 0, fds, len(fds))
|
|
299
|
+
for i in range(max(size, 0) // 8):
|
|
300
|
+
fd, kind = struct.unpack_from("<iI", fds, i * 8)
|
|
301
|
+
if kind != PROX_FDTYPE_SOCKET:
|
|
302
|
+
continue
|
|
303
|
+
if lib.proc_pidfdinfo(pid, fd, PROC_PIDFDSOCKETINFO, record, SOCKET_FDINFO_SIZE) != SOCKET_FDINFO_SIZE:
|
|
304
|
+
continue
|
|
305
|
+
parsed = parse_socket_fdinfo(record.raw)
|
|
306
|
+
if parsed is not None:
|
|
307
|
+
out.append((pid,) + parsed)
|
|
308
|
+
return out
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _calibrate_libproc() -> Optional[bool]:
|
|
312
|
+
"""Whether the `socket_fdinfo` layout reads back a connected pair of this process's own (the listener and both
|
|
313
|
+
ends, each once, with their exact addresses and this pid); None when libproc could not be read at all, which the
|
|
314
|
+
caller does not keep (FAA-273 item 11: an unread calibration is not a verdict)."""
|
|
315
|
+
probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
316
|
+
client = taken = None
|
|
317
|
+
try:
|
|
318
|
+
probe.bind(("127.0.0.1", 0))
|
|
319
|
+
probe.listen(1)
|
|
320
|
+
client = socket.create_connection(probe.getsockname(), timeout=TIMEOUT_SECONDS)
|
|
321
|
+
taken, _ = probe.accept()
|
|
322
|
+
server, near = probe.getsockname()[:2], client.getsockname()[:2]
|
|
323
|
+
have = own_sockets(_libproc_records(os.getuid(), time.monotonic() + TIMEOUT_SECONDS))
|
|
324
|
+
me = os.getpid()
|
|
325
|
+
return all(len(hits) == 1 and hits[0].pid == me for hits in (
|
|
326
|
+
[s for s in have if s.listening and s.local == server],
|
|
327
|
+
[s for s in have if not s.listening and s.local == server and s.remote == near],
|
|
328
|
+
[s for s in have if not s.listening and s.local == near and s.remote == server]))
|
|
329
|
+
except (ProofUnreadable, OSError, struct.error, ValueError):
|
|
330
|
+
return None
|
|
331
|
+
finally:
|
|
332
|
+
for s in (client, taken, probe):
|
|
333
|
+
if s is not None:
|
|
334
|
+
s.close()
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def method(platform: Optional[str] = None) -> str:
|
|
338
|
+
"""The reader the next question uses: libproc once its layout passed calibration, else lsof; none off macOS."""
|
|
339
|
+
if (platform or sys.platform) != "darwin":
|
|
340
|
+
return "none"
|
|
341
|
+
with _LOCK:
|
|
342
|
+
if _LIBPROC_OK[0] is None:
|
|
343
|
+
_LIBPROC_OK[0] = _calibrate_libproc()
|
|
344
|
+
return LIBPROC if _LIBPROC_OK[0] else LSOF_METHOD
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _ask(port: int, state: str, pick: Callable[[List[Socket], str], Proof], wait: float, platform: Optional[str],
|
|
348
|
+
uid: Optional[int], euid: Optional[int], how: Optional[str], run: Optional[Callable[[List[str]], str]]) -> Proof:
|
|
349
|
+
start = time.monotonic()
|
|
350
|
+
if (platform or sys.platform) != "darwin":
|
|
351
|
+
return Proof(False, None, "none", "%s: unsupported-platform" % TABLE_RESTRICTED)
|
|
352
|
+
uid = os.getuid() if uid is None else uid
|
|
353
|
+
euid = os.geteuid() if euid is None else euid
|
|
354
|
+
if uid != euid:
|
|
355
|
+
return Proof(False, None, how or "none", "%s: setuid-context" % TABLE_RESTRICTED)
|
|
356
|
+
how = how or method(platform)
|
|
357
|
+
pause = 0.005
|
|
358
|
+
while True:
|
|
359
|
+
try:
|
|
360
|
+
if how == LIBPROC:
|
|
361
|
+
have = own_sockets(_libproc_records(uid, start + TIMEOUT_SECONDS))
|
|
362
|
+
else:
|
|
363
|
+
argv = [LSOF, "-nP", "-a", "-u", str(uid), "-iTCP:%d" % port, "-sTCP:%s" % state, "-F", "pun"]
|
|
364
|
+
have = parse_lsof_sockets((run or _run)(argv), uid)
|
|
365
|
+
except ProofUnreadable as exc:
|
|
366
|
+
return Proof(False, None, how, "%s: %s" % (TABLE_RESTRICTED, exc))
|
|
367
|
+
answer = pick(have, how)
|
|
368
|
+
if answer.proved or time.monotonic() + pause > start + min(wait, TIMEOUT_SECONDS):
|
|
369
|
+
return answer
|
|
370
|
+
time.sleep(pause)
|
|
371
|
+
pause = min(pause * 2, 0.05)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def accepted(server: Address, client: Address, *, wait: bool = False, platform: Optional[str] = None,
|
|
375
|
+
uid: Optional[int] = None, euid: Optional[int] = None, how: Optional[str] = None,
|
|
376
|
+
run: Optional[Callable[[List[str]], str]] = None) -> Proof:
|
|
377
|
+
"""RT-2: whether the socket at `server` connected to `client` (a client's own connection, seen from the server's
|
|
378
|
+
side) is in one of this account's processes. `wait`: a server end is read again for ACCEPT_WAIT_SECONDS, since a
|
|
379
|
+
client asks right after connect() and the server's accepted socket exists only once it calls accept().
|
|
380
|
+
`platform`, `uid`, `euid`, `how` and `run` are the tests'."""
|
|
381
|
+
server, client = (_host(str(server[0])), int(server[1])), (_host(str(client[0])), int(client[1]))
|
|
382
|
+
return _ask(server[1], "ESTABLISHED", lambda have, how_: accepted_in(have, server, client, how_),
|
|
383
|
+
ACCEPT_WAIT_SECONDS if wait else 0.0, platform, uid, euid, how, run)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def own_listener(port: int, *, platform: Optional[str] = None, uid: Optional[int] = None, euid: Optional[int] = None,
|
|
387
|
+
how: Optional[str] = None, run: Optional[Callable[[List[str]], str]] = None) -> Proof:
|
|
388
|
+
"""FAA-272: whether one of this account's processes listens on exactly 127.0.0.1:<port>."""
|
|
389
|
+
return _ask(int(port), "LISTEN", lambda have, how_: listener_in(have, int(port), how_), 0.0, platform, uid, euid,
|
|
390
|
+
how, run)
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py
CHANGED
|
@@ -522,14 +522,9 @@ def _read_well_known(origin: str, timeout: float = 2.0,
|
|
|
522
522
|
def _job_listens(pid: int, port: int) -> bool:
|
|
523
523
|
"""Whether launchd's job `pid` listens on 127.0.0.1:<port> exactly (never the wildcard), asked of the process itself
|
|
524
524
|
(`lsof -p`), which a restricted socket table does not hide. Used only for a LaunchDaemon root just bootstrapped,
|
|
525
|
-
whose pid launchd itself named (FAA-278).
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
try:
|
|
529
|
-
out = _po._run([_po.LSOF, "-nP", "-a", "-p", str(pid), "-i4TCP", "-sTCP:LISTEN", "-Fn"])
|
|
530
|
-
except _po.TableUnreadable:
|
|
531
|
-
return False
|
|
532
|
-
return any(host == "127.0.0.1" and p == port for host, p in _po.parse_lsof_listeners(out)) # exact bind, as RT-3
|
|
525
|
+
whose pid launchd itself named (FAA-278). The peer owner's one lsof reader, the uid left to the caller, which checks
|
|
526
|
+
the job's UserName itself (FAA-286 item 1)."""
|
|
527
|
+
return _po.pid_listens(pid, port, owner=None)
|
|
533
528
|
|
|
534
529
|
|
|
535
530
|
def _account_uid(user_name: str) -> Optional[int]:
|