@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_package.py
CHANGED
|
@@ -390,12 +390,24 @@ def package_findings(pkg: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
|
390
390
|
|
|
391
391
|
# --- the folder check and the verifier ------------------------------------------------------------------
|
|
392
392
|
|
|
393
|
+
def _json_int(text: str) -> Any:
|
|
394
|
+
"""An integer literal as JSON.parse reads it: one longer than a Python with `int_max_str_digits` reads is a float,
|
|
395
|
+
as a JavaScript number is (FAA-194 (36))."""
|
|
396
|
+
return int(text) if len(text) <= 4000 else float(text)
|
|
397
|
+
|
|
398
|
+
|
|
393
399
|
def _parsed(data: Optional[bytes]) -> Optional[Dict[str, Any]]:
|
|
400
|
+
"""A plugin or provider manifest as the contract's checkPackageFolder reads it: decoded with replacement characters
|
|
401
|
+
(`Buffer.toString`, FAA-194 (29)); NaN or Infinity, or nesting past MAX_NESTING (the limit both kits share, FAA-194
|
|
402
|
+
(27)), is no manifest."""
|
|
394
403
|
if data is None:
|
|
395
404
|
return None
|
|
405
|
+
text = data.decode("utf-8", "replace")
|
|
406
|
+
if _nesting(text) > MAX_NESTING:
|
|
407
|
+
return None
|
|
396
408
|
try:
|
|
397
|
-
value = json.loads(
|
|
398
|
-
except (ValueError, RecursionError):
|
|
409
|
+
value = json.loads(text, parse_constant=fd._no_constant, parse_int=_json_int)
|
|
410
|
+
except (ValueError, RecursionError):
|
|
399
411
|
return None
|
|
400
412
|
return value if isinstance(value, dict) else None
|
|
401
413
|
|
|
@@ -419,7 +431,7 @@ def check_package_folder(pkg: Dict[str, Any], files: Dict[str, bytes]) -> List[D
|
|
|
419
431
|
problems.append({"problem": "file-unlisted", "path": path})
|
|
420
432
|
if PLUGIN_MANIFEST in files:
|
|
421
433
|
plugin = _parsed(files.get(PLUGIN_MANIFEST))
|
|
422
|
-
if
|
|
434
|
+
if plugin is None or not _same_member(plugin, "name", pkg.get("id", _UNDEFINED)) or not _same_member(plugin, "version", pkg.get("version", _UNDEFINED)):
|
|
423
435
|
problems.append({"problem": "plugin-manifest", "path": PLUGIN_MANIFEST})
|
|
424
436
|
provider = pkg.get("provider")
|
|
425
437
|
if isinstance(provider, str) and provider in files:
|
|
@@ -615,7 +627,7 @@ def read_folder(folder: Any) -> Dict[str, bytes]:
|
|
|
615
627
|
if _BY_FOLDER:
|
|
616
628
|
os.close(top) # type: ignore[arg-type]
|
|
617
629
|
if refused:
|
|
618
|
-
raise PackageError("Not packed:
|
|
630
|
+
raise PackageError("Not packed: pack a clean copy (what Git would commit, references/package.md); delete only litter and credential files in place: " + "; ".join(sorted(refused)))
|
|
619
631
|
return files
|
|
620
632
|
|
|
621
633
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py
CHANGED
|
@@ -38,23 +38,35 @@ def services_dir() -> Path:
|
|
|
38
38
|
return Path(base) / "passioncode-fabric/services"
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
def _durable(service_id: str, dirs: Dict[str, Path]) -> Dict[str, Path]:
|
|
42
|
+
"""FABRIC_DATA_HOME (FAA-192): an absolute folder on a durable volume holds the data folder, `<home>/<id>`, on a host
|
|
43
|
+
whose own disk does not persist; logs and cache stay where the OS puts them. A relative value is refused."""
|
|
44
|
+
value = os.environ.get("FABRIC_DATA_HOME")
|
|
45
|
+
if not value:
|
|
46
|
+
return dirs
|
|
47
|
+
if not os.path.isabs(value):
|
|
48
|
+
raise ServiceError("FABRIC_DATA_HOME is an absolute folder on a durable volume.")
|
|
49
|
+
return dict(dirs, data=Path(value) / service_id)
|
|
50
|
+
|
|
51
|
+
|
|
41
52
|
def service_dirs(service_id: str) -> Dict[str, Path]:
|
|
42
|
-
"""State lives outside code: data+config, logs and cache in per-user OS directories
|
|
53
|
+
"""State lives outside code: data+config, logs and cache in per-user OS directories (the data folder under
|
|
54
|
+
FABRIC_DATA_HOME when it is set)."""
|
|
43
55
|
_require_id(service_id)
|
|
44
56
|
home = Path.home()
|
|
45
57
|
if sys.platform == "darwin":
|
|
46
|
-
return {
|
|
58
|
+
return _durable(service_id, {
|
|
47
59
|
"data": home / "Library/Application Support" / service_id,
|
|
48
60
|
"logs": home / "Library/Logs" / service_id,
|
|
49
61
|
"cache": home / "Library/Caches" / service_id,
|
|
50
|
-
}
|
|
62
|
+
})
|
|
51
63
|
if sys.platform == "win32":
|
|
52
64
|
data = _local_app_data() / service_id
|
|
53
|
-
return {"data": data, "logs": data / "Logs", "cache": data / "Cache"}
|
|
65
|
+
return _durable(service_id, {"data": data, "logs": data / "Logs", "cache": data / "Cache"})
|
|
54
66
|
data = Path(os.environ.get("XDG_DATA_HOME") or home / ".local/share") / service_id
|
|
55
67
|
state = Path(os.environ.get("XDG_STATE_HOME") or home / ".local/state") / service_id
|
|
56
68
|
cache = Path(os.environ.get("XDG_CACHE_HOME") or home / ".cache") / service_id
|
|
57
|
-
return {"data": data, "logs": state / "logs", "cache": cache}
|
|
69
|
+
return _durable(service_id, {"data": data, "logs": state / "logs", "cache": cache})
|
|
58
70
|
|
|
59
71
|
|
|
60
72
|
def instance_dirs(service_id: str, instance: str = "default") -> Dict[str, Path]:
|
|
@@ -69,6 +81,9 @@ def instance_dirs(service_id: str, instance: str = "default") -> Dict[str, Path]
|
|
|
69
81
|
folder, personal = "%s.%s" % (service_id, instance), service_dirs(service_id)
|
|
70
82
|
if sys.platform == "win32":
|
|
71
83
|
data = personal["data"].parent / folder
|
|
84
|
+
if os.environ.get("FABRIC_DATA_HOME"): # logs and cache stay on the local disk, beside the default's
|
|
85
|
+
local = personal["logs"].parent.parent / folder
|
|
86
|
+
return {"data": data, "logs": local / "Logs", "cache": local / "Cache"}
|
|
72
87
|
return {"data": data, "logs": data / "Logs", "cache": data / "Cache"}
|
|
73
88
|
if sys.platform == "darwin":
|
|
74
89
|
return {key: path.parent / folder for key, path in personal.items()}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
"""peer_owner: which OS account owns each end of a loopback TCP connection.
|
|
2
2
|
|
|
3
|
-
peer_owner 1.
|
|
4
|
-
`observatory/engine/peer_owner.py`,
|
|
5
|
-
`fabric_peer_owner` (layer L0; DEC-0154 HA-2, FAA-226, FAA-258).
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
`
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
reader. `main` is the CLI, `listener <port>` (FAA-315). /proc carries a socket's creator, so a listener root created
|
|
21
|
-
and handed to another account (systemd socket activation, User=) reads as root's. Standard library only, no other
|
|
22
|
-
kit module. Tests: `test/test_fabric_peer_owner.py`; the Node twin is `fabric-peer-owner.mjs` (no ctypes).
|
|
3
|
+
peer_owner 1.3.1+kit.7: Observatory's peer_owner (passioncode-ai/project-observatory-dashboard,
|
|
4
|
+
`observatory/engine/peer_owner.py`, vendored at f86f0622, read against 2180235c for 1.2.0 and a69c6fee for 1.3.1),
|
|
5
|
+
the kit's `fabric_peer_owner` (layer L0; DEC-0154 HA-2, FAA-226, FAA-258). 1.3.1's proof from this account's own
|
|
6
|
+
sockets (DEC-0155 RT-2) is the kit's `fabric_socket_proof`, asked when it sits beside this file (FAA-292); without
|
|
7
|
+
it this file stands alone, as before. The kit's differences: NamedTuples (a service may load a kit file by path);
|
|
8
|
+
the Python 3.9 floor; `classify` / `classify_sid` ask for this process's uid or SID first (a client running as root
|
|
9
|
+
owns root's listener); on Linux `server_end` reads an accepted connection's own row and, for one not accept()ed yet
|
|
10
|
+
(uid 0, inode 0), the listener on its local address (CT-254), several rows of one owner are that owner, a row
|
|
11
|
+
printed twice counts once, and a `/proc` question that finds no row or several reads once more. On macOS (FAA-267,
|
|
12
|
+
FAA-273): the sysctl calibration also requires a child's listener, read under one deadline with `selectors`; a
|
|
13
|
+
number is ASCII digits only; a caller may vouch for a listener with a pid it started (`pid_listens`,
|
|
14
|
+
`CheckedHTTPConnection(vouch_pid=...)`, RT-3, the fallback after RT-2). DEC-0155 (FAA-312): a netstat that fails or
|
|
15
|
+
is empty, a table with no row of another uid (sysctl) or process (netstat) and a missing connection row read as
|
|
16
|
+
`table-restricted` (RT-1); a restricted table's listener is this account's only on exactly 127.0.0.1 (RT-4); no
|
|
17
|
+
proof is taken from a set-uid reader. `main` is the CLI, `listener <port>` (FAA-315). /proc carries a socket's
|
|
18
|
+
creator, so a listener root created and handed to another account (socket activation) reads as root's. Tests:
|
|
19
|
+
`test/test_fabric_peer_owner.py`; the Node twin is `fabric-peer-owner.mjs`.
|
|
23
20
|
|
|
24
21
|
WHY. Loopback is one per machine, not one per OS account. On a host with several accounts, every account's processes
|
|
25
22
|
reach every service any account listens with on 127.0.0.1, and a loopback socket proves nothing about who is on the
|
|
@@ -29,17 +26,14 @@ other end. This module answers that question from the host's socket table, read
|
|
|
29
26
|
server_end(sock) -> Owner for a client's connected socket: who accepted it
|
|
30
27
|
peer(sock) -> Owner for a server's accepted socket: who connected
|
|
31
28
|
peer_at(server, client) the same as `peer`, from two (host, port) addresses
|
|
32
|
-
own_connection(port, ...) a CheckedHTTPConnection, connected: every connect
|
|
33
|
-
|
|
34
|
-
that the server end is this account's, or raises ListenerRefused
|
|
35
|
-
before a byte of the request is sent
|
|
29
|
+
own_connection(port, ...) a CheckedHTTPConnection, connected: every connect (the first, and any reconnect
|
|
30
|
+
http.client makes) checks the server end is this account's before a byte is sent
|
|
36
31
|
|
|
37
32
|
`Owner.kind` is one of:
|
|
38
33
|
|
|
39
34
|
this-account the socket belongs to the account running this process
|
|
40
35
|
another-account another OS account's
|
|
41
|
-
system uid 0, pid 1 (launchd
|
|
42
|
-
Windows, the system's own accounts and processes
|
|
36
|
+
system uid 0, pid 1 (launchd's socket-activated jobs) or, on Windows, the system's accounts and processes
|
|
43
37
|
unknown the owner could not be told; `reason` says why
|
|
44
38
|
none `listener` only: nothing listens on that port
|
|
45
39
|
|
|
@@ -52,36 +46,28 @@ answer; two live rows with the same addresses are `unknown` (`several-rows`), ne
|
|
|
52
46
|
|
|
53
47
|
- macOS, primary (`sysctl`): `sysctl net.inet.tcp.pcblist_n` through ctypes, the record list `netstat` itself reads.
|
|
54
48
|
Each connection is an `xinpcb_n` (addresses and ports) followed by an `xsocket_n` that carries `so_uid` and
|
|
55
|
-
`so_last_pid` (XNU `bsd/netinet/in_pcb.h` and `bsd/sys/socketvar.h`, both under `#pragma pack(4)`)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`table-restricted`, never another account's. A caller that started the server (an installer, its launchd job) may
|
|
65
|
-
vouch with that pid: `lsof -p PID` asks the process, which the restriction leaves readable (RT-3).
|
|
49
|
+
`so_last_pid` (XNU `bsd/netinet/in_pcb.h` and `bsd/sys/socketvar.h`, both under `#pragma pack(4)`), paired by the pcb
|
|
50
|
+
handle (`xi_inpp` equals `so_pcb`); the `xtcpcb_n` that follows gives the TCP state. Measured on macOS 26: about 4 ms
|
|
51
|
+
for 770 sockets. The layout is not an API, so the first read in a process calibrates it on a connected pair of its
|
|
52
|
+
own (the listener and both ends must read back with their exact addresses, this uid and this pid); a layout that
|
|
53
|
+
fails is not used again in that process.
|
|
54
|
+
- macOS 27.2 shows an ad-hoc-signed interpreter (a Homebrew Python) only its own sockets: the calibration also requires
|
|
55
|
+
a child's listener, and a restricted table (DEC-0155 RT-1) is `unknown` with a reason beginning `table-restricted`,
|
|
56
|
+
never another account's, unless `fabric_socket_proof` finds the socket in this account's own processes (RT-2) or a
|
|
57
|
+
caller that started the server vouches with its pid, which `lsof -p PID` asks the process itself about (RT-3).
|
|
66
58
|
- macOS, fallback (`netstat`): `/usr/sbin/netstat -anv -p tcp`, its `process:pid` column, then `ps -o uid= -p PID` for
|
|
67
|
-
the
|
|
68
|
-
|
|
69
|
-
way; it is kept as the documented fallback for a macOS whose record layout the calibration refuses.
|
|
59
|
+
the one row a question needs; about 20 ms over 846 sockets, and it names the pids of root's listeners, which `lsof`
|
|
60
|
+
run by an ordinary account cannot see. Kept for a macOS whose record layout the calibration refuses.
|
|
70
61
|
- Linux (`proc`): `/proc/net/tcp` and `/proc/net/tcp6`, whose `uid` column every account may read (also under
|
|
71
|
-
`hidepid=2
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
`GetTokenInformation(TokenUser)` give its account SID, compared with this process's. A process that cannot be opened
|
|
76
|
-
(another account's, usually) is `unknown`, never `another-account`: the caller refuses either way, and the reason
|
|
77
|
-
says which it was. `Owner.uid` is None there.
|
|
62
|
+
`hidepid=2`). No pid: that is only readable for this account's own processes, and the decision needs the uid alone.
|
|
63
|
+
- Windows (`iphlpapi`): `GetExtendedTcpTable(TCP_TABLE_OWNER_PID_ALL)` gives each row's owning pid; `OpenProcess`,
|
|
64
|
+
`OpenProcessToken` and `GetTokenInformation(TokenUser)` its account SID, compared with this process's. A process that
|
|
65
|
+
cannot be opened (another account's, usually) is `unknown`, never `another-account`. `Owner.uid` is None there.
|
|
78
66
|
- Anything else: `unknown`, reason `unsupported-platform`.
|
|
79
67
|
|
|
80
|
-
`LOCAL_PEERCRED`, `getpeereid` and
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
WHAT IT NEVER DOES. It never reads a byte from the socket it is asked about, never resolves a name, never sends
|
|
84
|
-
anything, and never needs a privilege. It reports the process names `netstat` prints only as far as the pid behind them.
|
|
68
|
+
`LOCAL_PEERCRED`, `getpeereid` and `SO_PEERCRED` answer this for Unix-domain sockets only, hence a socket table. It
|
|
69
|
+
never reads a byte from the socket it is asked about, never resolves a name, never sends anything, never needs a
|
|
70
|
+
privilege, and reports the process names `netstat` prints only as far as the pid behind them.
|
|
85
71
|
"""
|
|
86
72
|
from __future__ import annotations
|
|
87
73
|
|
|
@@ -98,7 +84,7 @@ import time
|
|
|
98
84
|
from typing import NamedTuple, Optional, Tuple
|
|
99
85
|
|
|
100
86
|
# This file's own version, for projects that copy it; not the engine's (that is configuration.VERSION).
|
|
101
|
-
PEER_OWNER_VERSION = "1.
|
|
87
|
+
PEER_OWNER_VERSION = "1.3.1+kit.7" # Observatory's 1.3.1 line with the kit's differences above
|
|
102
88
|
|
|
103
89
|
THIS_ACCOUNT = "this-account"
|
|
104
90
|
ANOTHER_ACCOUNT = "another-account"
|
|
@@ -201,11 +187,6 @@ def _posix_uid() -> Optional[int]:
|
|
|
201
187
|
return os.getuid() if hasattr(os, "getuid") else None
|
|
202
188
|
|
|
203
189
|
|
|
204
|
-
def _own_pid() -> int:
|
|
205
|
-
"""This process's pid: a netstat that shows no other process's row is a restricted table (DEC-0155 RT-1)."""
|
|
206
|
-
return os.getpid()
|
|
207
|
-
|
|
208
|
-
|
|
209
190
|
def _set_uid() -> bool:
|
|
210
191
|
"""Real and effective uid differ: such a reader uses none of DEC-0155's proofs (RT-2 to RT-5), the vouch included."""
|
|
211
192
|
return hasattr(os, "geteuid") and os.geteuid() != _posix_uid()
|
|
@@ -529,10 +510,9 @@ def _child_line(child: "subprocess.Popen[bytes]", deadline: float) -> Optional[b
|
|
|
529
510
|
|
|
530
511
|
|
|
531
512
|
def _child_listener_seen(rows_of) -> Optional[bool]:
|
|
532
|
-
"""Whether the table shows a listener of a child process of this one: False when it
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
pipe that cannot be read). The calibration trusts only True."""
|
|
513
|
+
"""Whether the table shows a listener of a child process of this one: False when not (it shows this process alone,
|
|
514
|
+
FAA-267), None when no child could be started (a frozen or embedded host) or it gave no port line within
|
|
515
|
+
TIMEOUT_SECONDS (silence, a partial line, an early exit, an unreadable pipe). The calibration trusts only True."""
|
|
536
516
|
if not sys.executable or getattr(sys, "frozen", False):
|
|
537
517
|
return None
|
|
538
518
|
try:
|
|
@@ -780,7 +760,7 @@ def restricted(rows: list[Row], how: str, *, own_uid: Optional[int] = None, own_
|
|
|
780
760
|
own = _posix_uid() if own_uid is None else own_uid
|
|
781
761
|
return not any(row.uid is not None and row.uid != own for row in rows)
|
|
782
762
|
if how == "netstat":
|
|
783
|
-
own =
|
|
763
|
+
own = os.getpid() if own_pid is None else own_pid
|
|
784
764
|
return not any(row.pid is not None and row.pid != own for row in rows)
|
|
785
765
|
return False
|
|
786
766
|
|
|
@@ -810,10 +790,9 @@ def _owner_of(local: tuple[str, int], remote: tuple[str, int], server: bool = Fa
|
|
|
810
790
|
if len(found) > 1:
|
|
811
791
|
return Owner(UNKNOWN, None, None, how, "several-rows")
|
|
812
792
|
if how == "proc" and server and found[0].uid is None:
|
|
813
|
-
# Linux: a connection not accept()ed yet (uid 0, inode 0) belongs to the listener on its local address (exact,
|
|
814
|
-
#
|
|
815
|
-
#
|
|
816
|
-
# 1.2.0, board thread ced00f6e; CT-254). An orphaned server row reads so too: nothing sent to it reaches anyone.
|
|
793
|
+
# Linux: a connection not accept()ed yet (uid 0, inode 0) belongs to the listener on its local address (exact, else
|
|
794
|
+
# the wildcard) and dies with it; an accepted one's own row carries the accepting process's uid (sock_graft), so a
|
|
795
|
+
# listener swapped after another account accepted cannot pass (CT-254). An orphaned server row reads so too.
|
|
817
796
|
return _one_owner(_listeners_of(rows, found[0].local), how, "not-accepted")
|
|
818
797
|
if how == "proc" and found[0].uid is None:
|
|
819
798
|
# A client's socket with inode 0 has already closed (0.74.0 unit reading 4): never another socket's owner.
|
|
@@ -842,10 +821,32 @@ def _one_owner(found: list[Row], how: str, none_reason: Optional[str] = None) ->
|
|
|
842
821
|
return Owner(UNKNOWN, None, None, how, "several-rows")
|
|
843
822
|
|
|
844
823
|
|
|
824
|
+
_PROOF: list = [None] # fabric_socket_proof once looked for: None not yet, False not beside this file (FAA-292)
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def _proved(ask: str, *args, fallback: Owner, **kwargs) -> Owner:
|
|
828
|
+
"""`fallback`, unless it is a restricted macOS table's `unknown` and `fabric_socket_proof` beside this file finds the
|
|
829
|
+
socket (`accepted`) or the 127.0.0.1 listener (`own_listener`, FAA-272) in this account's processes: else it stands."""
|
|
830
|
+
if fallback.kind != UNKNOWN or not (fallback.reason or "").startswith(TABLE_RESTRICTED) or sys.platform != "darwin":
|
|
831
|
+
return fallback
|
|
832
|
+
try:
|
|
833
|
+
if _PROOF[0] is None:
|
|
834
|
+
sys.path.append(os.path.dirname(os.path.abspath(__file__))) # beside this file, as the kit's modules load
|
|
835
|
+
import fabric_socket_proof
|
|
836
|
+
_PROOF[0] = fabric_socket_proof
|
|
837
|
+
proof = getattr(_PROOF[0], ask)(*args, **kwargs) if _PROOF[0] else None
|
|
838
|
+
except ImportError:
|
|
839
|
+
_PROOF[0], proof = False, None
|
|
840
|
+
except Exception: # noqa: BLE001 - a proof that fails proves nothing; the table's answer stands (fail closed)
|
|
841
|
+
proof = None
|
|
842
|
+
return Owner(THIS_ACCOUNT, _posix_uid(), proof.pid, proof.method, proof.reason) if proof and proof.proved else fallback
|
|
843
|
+
|
|
844
|
+
|
|
845
845
|
def peer_at(server: tuple, client: tuple) -> Owner:
|
|
846
846
|
"""The owner of the CLIENT socket of the connection between `server` and `client`, as a server sees them (its own
|
|
847
847
|
address, the peer's): the row whose local address is the client's and whose remote address is the server's."""
|
|
848
|
-
|
|
848
|
+
client, server = _address(client), _address(server)
|
|
849
|
+
return _proved("accepted", client, server, wait=False, fallback=_owner_of(client, server))
|
|
849
850
|
|
|
850
851
|
|
|
851
852
|
def peer(sock: socket.socket) -> Owner:
|
|
@@ -859,14 +860,14 @@ def peer(sock: socket.socket) -> Owner:
|
|
|
859
860
|
|
|
860
861
|
def server_end(sock: socket.socket) -> Owner:
|
|
861
862
|
"""For a client's connected socket: who accepted it. The row whose local address is the server's and whose remote
|
|
862
|
-
address is this socket's own,
|
|
863
|
-
|
|
864
|
-
its listener answers (the contract's CT-254, HA-2's Linux source)."""
|
|
863
|
+
address is this socket's own, the server process's accepted socket, so a listener swapped after `listen` cannot
|
|
864
|
+
pass; on Linux a connection not accept()ed yet answers through its listener (CT-254); RT-2 under a restricted table."""
|
|
865
865
|
try:
|
|
866
866
|
own, server = sock.getsockname(), sock.getpeername()
|
|
867
867
|
except OSError as exc:
|
|
868
868
|
return Owner(UNKNOWN, None, None, "none", f"socket: {type(exc).__name__}")
|
|
869
|
-
|
|
869
|
+
server, own = _address(server), _address(own)
|
|
870
|
+
return _proved("accepted", server, own, wait=True, fallback=_owner_of(server, own, server=True))
|
|
870
871
|
|
|
871
872
|
|
|
872
873
|
#: port -> (time.monotonic() of the read, the Owner)
|
|
@@ -899,6 +900,7 @@ def listener(port: int) -> Owner:
|
|
|
899
900
|
owner = Owner(UNKNOWN, None, None, method(), str(exc))
|
|
900
901
|
else:
|
|
901
902
|
owner = _restricted_listener(rows, port, how) if restricted(rows, how) else _one_owner(pick_listener(rows, port), how)
|
|
903
|
+
owner = _proved("own_listener", port, fallback=owner)
|
|
902
904
|
_LISTENERS[port] = (now, owner)
|
|
903
905
|
return owner
|
|
904
906
|
|
|
@@ -922,17 +924,21 @@ def refusal(owner: Owner, port: int, expect_pid: Optional[int] = None) -> Option
|
|
|
922
924
|
return None
|
|
923
925
|
|
|
924
926
|
|
|
925
|
-
def pid_listens(pid: int, port: int) -> bool:
|
|
926
|
-
"""macOS,
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
if
|
|
927
|
+
def pid_listens(pid: int, port: int, owner: object = THIS_ACCOUNT) -> bool:
|
|
928
|
+
"""macOS, a restricted table (FAA-267, DEC-0155 RT-3): whether process `pid` listens on exactly 127.0.0.1:<port>
|
|
929
|
+
(never the IPv4 wildcard, which `lsof` prints as an IPv6-only one, nor ::1), asked of the process (`lsof -p`). Only
|
|
930
|
+
a caller that started `pid` (a launchd job it installed) vouches with it, never a set-uid one for this account. `owner`:
|
|
931
|
+
the uid `pid` runs as, this account's by default, or None when the caller checks it (a LaunchDaemon's UserName)."""
|
|
932
|
+
if owner is not None and owner != THIS_ACCOUNT and (not isinstance(owner, int) or isinstance(owner, bool) or owner < 0):
|
|
933
|
+
raise ValueError("owner is this-account, a uid or None")
|
|
934
|
+
if sys.platform != "darwin" or not isinstance(pid, int) or isinstance(pid, bool) or pid <= 1 or (
|
|
935
|
+
owner == THIS_ACCOUNT and _set_uid()):
|
|
931
936
|
return False
|
|
932
937
|
try:
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
938
|
+
if owner is not None:
|
|
939
|
+
uid = parse_ps_uid(_run([PS, "-o", "uid=", "-p", str(pid)]))
|
|
940
|
+
if uid is None or uid != (_posix_uid() if owner == THIS_ACCOUNT else owner):
|
|
941
|
+
return False
|
|
936
942
|
out = _run([LSOF, "-nP", "-a", "-p", str(pid), "-i4TCP", "-sTCP:LISTEN", "-Fn"])
|
|
937
943
|
except TableUnreadable:
|
|
938
944
|
return False
|
|
@@ -940,9 +946,8 @@ def pid_listens(pid: int, port: int) -> bool:
|
|
|
940
946
|
|
|
941
947
|
|
|
942
948
|
class CheckedHTTPConnection(http.client.HTTPConnection):
|
|
943
|
-
"""An `http.client.HTTPConnection` whose every `connect()`
|
|
944
|
-
|
|
945
|
-
listener. A refused connect closes the socket and raises ListenerRefused before any byte of a request is sent."""
|
|
949
|
+
"""An `http.client.HTTPConnection` whose every `connect()` (the reconnect `auto_open` makes too, which may reach
|
|
950
|
+
another listener) checks the server end: refused, it closes the socket and raises ListenerRefused before a byte."""
|
|
946
951
|
|
|
947
952
|
def __init__(self, host: str, port: int, *, expect_pid: Optional[int] = None, timeout: float = 10.0,
|
|
948
953
|
vouch_pid: Optional[int] = None) -> None:
|
|
@@ -957,7 +962,7 @@ class CheckedHTTPConnection(http.client.HTTPConnection):
|
|
|
957
962
|
super().connect()
|
|
958
963
|
owner = server_end(self.sock)
|
|
959
964
|
if (owner.kind == UNKNOWN and (owner.reason or "").startswith(TABLE_RESTRICTED) and getattr(self, "vouch_pid", None)
|
|
960
|
-
and pid_listens(self.vouch_pid, self.port)):
|
|
965
|
+
and _address(self.sock.getpeername())[0] == "127.0.0.1" and pid_listens(self.vouch_pid, self.port)):
|
|
961
966
|
# The table hides other processes (FAA-267); the process this caller started holds the listener.
|
|
962
967
|
owner = Owner(THIS_ACCOUNT, _posix_uid(), self.vouch_pid, "lsof-pid")
|
|
963
968
|
refused = refusal(owner, self.port, self.expect_pid)
|
|
@@ -969,14 +974,9 @@ class CheckedHTTPConnection(http.client.HTTPConnection):
|
|
|
969
974
|
def own_connection(port: int, *, expect_pid: Optional[int] = None, timeout: float = 10.0,
|
|
970
975
|
host: str = "127.0.0.1", vouch_pid: Optional[int] = None) -> CheckedHTTPConnection:
|
|
971
976
|
"""A `CheckedHTTPConnection` to `host:port`, connected, whose server end is this account's (and, with `expect_pid`,
|
|
972
|
-
that process's)
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
server-not-own another account's or the system's process accepted it
|
|
977
|
-
server-unverified its owner could not be told (nothing is sent: fail closed)
|
|
978
|
-
server-not-this-workspace this account's process, but not the pid expected
|
|
979
|
-
|
|
977
|
+
that process's), checked on the very connection the caller sends on and on every reconnect; no proxy is consulted.
|
|
978
|
+
ListenerRefused, the connection closed: `server-not-own` (another account's or the system's process accepted it),
|
|
979
|
+
`server-unverified` (its owner could not be told: fail closed), `server-not-this-workspace` (not the pid expected).
|
|
980
980
|
`vouch_pid`, as on `CheckedHTTPConnection` and the Node twin's `checkedSocket`: a pid the caller started (FAA-267)."""
|
|
981
981
|
conn = CheckedHTTPConnection(host, port, expect_pid=expect_pid, timeout=timeout, vouch_pid=vouch_pid)
|
|
982
982
|
conn.connect()
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py
CHANGED
|
@@ -31,7 +31,7 @@ CREDENTIAL_PAIRS: Dict[str, tuple] = {
|
|
|
31
31
|
}
|
|
32
32
|
#: ECMAScript's \s, as a character class body.
|
|
33
33
|
JS_SPACE = "\t\n\x0b\x0c\r \xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
|
|
34
|
-
|
|
34
|
+
NOT_LINE_END = "[^\n\r\u2028\u2029]" # ECMAScript's `.` without the s flag
|
|
35
35
|
JWT_SHAPE = re.compile(r"eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*")
|
|
36
36
|
_URL_BASE64_RUN = re.compile(r"[A-Za-z0-9_-]*")
|
|
37
37
|
|
|
@@ -74,14 +74,17 @@ _PROVIDER_KEYS = (r"(?:sk-(?=[A-Za-z0-9_-]{0,255}?[0-9])(?:(?=[A-Za-z0-9_-]{0,25
|
|
|
74
74
|
#: The contract's SECRET_VALUE at the pin (DEC-0151): `Bearer ` at a value's start in any case, or a provider key after a
|
|
75
75
|
#: boundary. No case-insensitive flag; digits are ASCII. A match includes its boundary character: callers test, never
|
|
76
76
|
#: read the matched text or its index.
|
|
77
|
-
SECRET_VALUE = re.compile("(^[Bb][Ee][Aa][Rr][Ee][Rr][" + JS_SPACE + "]" +
|
|
77
|
+
SECRET_VALUE = re.compile("(^[Bb][Ee][Aa][Rr][Ee][Rr][" + JS_SPACE + "]" + NOT_LINE_END + "+|" + _KEY_BOUNDARY + _PROVIDER_KEYS + ")")
|
|
78
78
|
_LONG_RUN = re.compile(r"[A-Za-z0-9+/=_-]{40,}")
|
|
79
|
-
|
|
79
|
+
BEARER_INSIDE = re.compile(r"(?<![A-Za-z0-9_])bearer[%s]+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{12,}=*" % JS_SPACE,
|
|
80
80
|
re.IGNORECASE | re.ASCII)
|
|
81
81
|
_PATH_LIKE = re.compile(r"^(?:[/~.]|[A-Za-z]:[\\/]|[a-z][a-z0-9+.-]*://)", re.IGNORECASE | re.ASCII)
|
|
82
82
|
_WORD_SPLIT = re.compile("[%s._-]+" % JS_SPACE)
|
|
83
83
|
_ARGUMENT_SPLIT = re.compile("[%s=]" % JS_SPACE)
|
|
84
|
-
|
|
84
|
+
SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
|
|
85
|
+
#: The names the check and older readers wrote before these became public (FAA-190 (2), (12)): kept so a module from
|
|
86
|
+
#: an earlier version beside this one still loads.
|
|
87
|
+
_BEARER_INSIDE, _SCHEME, _NOT_LINE_END = BEARER_INSIDE, SCHEME, NOT_LINE_END
|
|
85
88
|
#: WHATWG's special schemes that carry an authority with user information (file has none).
|
|
86
89
|
_SPECIAL = ("ftp", "http", "https", "ws", "wss")
|
|
87
90
|
|
|
@@ -108,13 +111,17 @@ def credential_name(name: str) -> bool:
|
|
|
108
111
|
return any(word in words and any(p in words for p in partners) for word, partners in CREDENTIAL_PAIRS.items())
|
|
109
112
|
|
|
110
113
|
|
|
111
|
-
def
|
|
114
|
+
def long_secret(value: str) -> bool:
|
|
115
|
+
"""A run of 40 or more base64 characters mixing upper case, lower case and digits (the Node twin's longSecret)."""
|
|
112
116
|
return any(re.search(r"[a-z]", run) and re.search(r"[A-Z]", run) and re.search(r"[0-9]", run) for run in _LONG_RUN.findall(value))
|
|
113
117
|
|
|
114
118
|
|
|
119
|
+
_long_secret = long_secret # its name before FAA-190 (2)
|
|
120
|
+
|
|
121
|
+
|
|
115
122
|
def secret_value(value: str) -> bool:
|
|
116
123
|
"""A JWT anywhere, a provider key or `Bearer …`, or a long base64 run mixing upper case, lower case and digits."""
|
|
117
|
-
return bool(has_jwt(value) or SECRET_VALUE.search(value) or
|
|
124
|
+
return bool(has_jwt(value) or SECRET_VALUE.search(value) or long_secret(value))
|
|
118
125
|
|
|
119
126
|
|
|
120
127
|
def _path_like(piece: str) -> bool:
|
|
@@ -126,13 +133,13 @@ def _path_like(piece: str) -> bool:
|
|
|
126
133
|
def secret_argument(value: str) -> bool:
|
|
127
134
|
"""The same for a launch argument or a server's command and URL: a long run is looked for between path separators,
|
|
128
135
|
so a folder path holding a session id is not taken for a key."""
|
|
129
|
-
if has_jwt(value) or SECRET_VALUE.search(value) or
|
|
136
|
+
if has_jwt(value) or SECRET_VALUE.search(value) or BEARER_INSIDE.search(value):
|
|
130
137
|
return True
|
|
131
138
|
for piece in _ARGUMENT_SPLIT.split(value):
|
|
132
139
|
if _path_like(piece):
|
|
133
|
-
if any(
|
|
140
|
+
if any(long_secret(part) for part in re.split(r"[\\/]", piece)):
|
|
134
141
|
return True
|
|
135
|
-
elif
|
|
142
|
+
elif long_secret(piece):
|
|
136
143
|
return True
|
|
137
144
|
return False
|
|
138
145
|
|
|
@@ -421,7 +428,7 @@ def _url_credential(value: str, hosts: bool) -> bool:
|
|
|
421
428
|
# What WHATWG URL parsing does first: trim C0 controls and space from the ends (never other Unicode spaces, which it
|
|
422
429
|
# refuses), then remove every tab, CR and LF.
|
|
423
430
|
value = re.sub(r"[\t\n\r]", "", value.strip(_C0_OR_SPACE))
|
|
424
|
-
if not
|
|
431
|
+
if not SCHEME.match(value):
|
|
425
432
|
return False # not an absolute URL (WHATWG URL parsing refuses it)
|
|
426
433
|
if hosts and url_refused(value):
|
|
427
434
|
return False # `new URL` refuses its host or port, so the Node twin reads it as an argument only (FAA-309)
|
|
@@ -442,7 +449,25 @@ _REPOSITORY_AUTHORITY = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://")
|
|
|
442
449
|
#: boundary, so composed after a separator look-behind it would miss a key after `-`, `.` or `_`.
|
|
443
450
|
_REPOSITORY_KEYS = r"sk-[A-Za-z0-9_-]{16,}|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,}"
|
|
444
451
|
_KEY_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))(?:%s)" % _REPOSITORY_KEYS)
|
|
445
|
-
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def jwt_at_a_word(text: str) -> bool:
|
|
455
|
+
"""Whether JWT_SHAPE matches at a word's start: the text's start, or after a `.`, `_` or `-` (FAA-321). Read in one
|
|
456
|
+
pass, as jwt_spans: the look-behind expression backtracked from every such `eyJ` of a long run, quadratic in it.
|
|
457
|
+
Every `eyJ` of one run shares that run's end and a shorter first segment, so only a run's first `eyJ` at a word's
|
|
458
|
+
start is tried; an `eyJ` that is not at one is passed over, since a later one of its run may be."""
|
|
459
|
+
at = text.find("eyJ")
|
|
460
|
+
while at >= 0:
|
|
461
|
+
if at and text[at - 1] not in "._-":
|
|
462
|
+
at = text.find("eyJ", at + 1)
|
|
463
|
+
continue
|
|
464
|
+
one = _URL_BASE64_RUN.match(text, at + 3).end()
|
|
465
|
+
if one - at - 3 >= 2 and text[one:one + 1] == ".":
|
|
466
|
+
two = _URL_BASE64_RUN.match(text, one + 1).end()
|
|
467
|
+
if two - one - 1 >= 2 and text[two:two + 1] == ".":
|
|
468
|
+
return True
|
|
469
|
+
at = text.find("eyJ", one)
|
|
470
|
+
return False
|
|
446
471
|
|
|
447
472
|
|
|
448
473
|
def repository_name(piece: str) -> bool:
|
|
@@ -450,7 +475,7 @@ def repository_name(piece: str) -> bool:
|
|
|
450
475
|
key's or JWT's shape (`sk-`, `ghp_`, `xoxb-`, `AKIA`, `eyJ`) at its start or after a `.`, `_` or `-` (0.62.0
|
|
451
476
|
readings 9 and 10), so a letters-only key is still a key while `task-pipeline-dashboard`, which holds an `sk-` shape
|
|
452
477
|
inside a word, is a name."""
|
|
453
|
-
return bool(_REPOSITORY_NAME.match(piece)) and not _KEY_AT_A_WORD.search(piece) and not
|
|
478
|
+
return bool(_REPOSITORY_NAME.match(piece)) and not _KEY_AT_A_WORD.search(piece) and not jwt_at_a_word(piece)
|
|
454
479
|
|
|
455
480
|
|
|
456
481
|
def ascii_decoded(text: str) -> str:
|