@passioncode-ai/fabric-agent-adapter 0.78.0 → 0.80.2
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 +256 -0
- package/README.md +14 -7
- package/bin/fabric-agent-adapter.js +26 -4
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +6 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +42 -7
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +13 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +3 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +12 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +23 -8
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +6 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +6 -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 +34 -8
- 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/dashboard-only/README.md +67 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/dashboard_service.py +252 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.mjs +168 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.py +179 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/README.md +57 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.mjs +181 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.py +237 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +11 -6
- 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 +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +42 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +12 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +26 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +14 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +44 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +12 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +7 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +10 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +15 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +37 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +30 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +48 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +9 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +344 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +54 -20
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +4 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +8 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +4 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +8 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +55 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +8 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +16 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +97 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +50 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +22 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +49 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +9 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +358 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +41 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +17 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +24 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +110 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +12 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +543 -37
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +4 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +12 -5
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +42 -7
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +6 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +13 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +13 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +32 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/working-rules.md +54 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +12 -4
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +27 -26
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/cleanup.md +30 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +59 -38
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +52 -26
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/remote-sessions.md +75 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +28 -13
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/task-context.md +58 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/waits.md +36 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +14 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +169 -46
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +146 -19
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +78 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +9 -2
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""The call queue and the load report of DEC-0104 (contract `src/agent-network.ts`, FAC-SEM-131 to FAC-SEM-133), as the
|
|
2
|
+
contract's reference answers them: a node that hosts a job-backed agent queues its jobs per (agentRef, context) with
|
|
3
|
+
these and reports its load as `service-load/1` under `extensions/load/0.1`. Part of the service kit (FAA-289, layer L3);
|
|
4
|
+
the twin of fabric-load.mjs. Standard library only. Nothing here raises on what it reads, except `load_extension`, which
|
|
5
|
+
refuses a declaration a reader would treat as not declared.
|
|
6
|
+
|
|
7
|
+
- job_caller_key(accepted, credential): the opaque key of a job's caller (an accepted delegation's principal, its
|
|
8
|
+
outermost actor and a partner organization ending the chain; else the credential the node knows the caller by).
|
|
9
|
+
- next_queued_job(queue, running, limits): the job the node starts next, or None (fairness between callers).
|
|
10
|
+
- admit_job(queue, running, job, limits, retry_after_seconds): {admit: True}, or the `queue-full` refusal with its data.
|
|
11
|
+
- queue_status_message(position, length), read_queue_position(status_message): `queued <position> of <length>`.
|
|
12
|
+
- job_queue_rules(value): FAC-SEM-131 over {queue, running, limits, started}.
|
|
13
|
+
- load_report_rules(report): FAC-SEM-132 on a `service-load/1` report; load_report_problems(report): its schema.
|
|
14
|
+
- load_view(well_known), load_extension_rules(well_known): the declaration as a reader reads it, and FAC-SEM-133.
|
|
15
|
+
- load_extension(path, window_seconds, limits): the `extensions` entry a service declares.
|
|
16
|
+
|
|
17
|
+
The kit has no JSON Schema validator: the two shapes are written here from the schemas as the contract's validator
|
|
18
|
+
reads them (ECMAScript's \\s in patterns, lengths in code points, `date-time` as ajv-formats' full mode). Times in the
|
|
19
|
+
queue are UTC instants (`interop-common#/$defs/utcInstant`); times in the report are RFC 3339 read strictly (the
|
|
20
|
+
contract's strictTime). The wording of a schema problem is the kit's, not ajv's."""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import math
|
|
24
|
+
import re
|
|
25
|
+
from typing import Any, Dict, List, Optional
|
|
26
|
+
|
|
27
|
+
import sys as _sys # noqa: E402
|
|
28
|
+
from pathlib import Path as _Path # noqa: E402
|
|
29
|
+
if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from any working directory (FAA-65)
|
|
30
|
+
_sys.path.insert(0, str(_Path(__file__).resolve().parent))
|
|
31
|
+
|
|
32
|
+
from fabric_base import ServiceError # noqa: E402
|
|
33
|
+
from fabric_connections import date_time # noqa: E402 - ajv-formats' date-time, as the contract's validator reads it
|
|
34
|
+
from fabric_jsvalue import ( # noqa: E402
|
|
35
|
+
MISSING, _JS_SPACE, double, get, is_number, json_stringify, objects, safe_integer, same, show,
|
|
36
|
+
)
|
|
37
|
+
from fabric_time import parse_rfc3339_ms # noqa: E402 - the contract's strictTime
|
|
38
|
+
|
|
39
|
+
LOAD_EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/load/0.1"
|
|
40
|
+
LOAD_REPORT_TYPE = "service-load/1"
|
|
41
|
+
#: The refusal code, the same as fabric_interop_rules.QUEUE_FULL (a test holds them equal); not imported because the
|
|
42
|
+
#: facade fabric_service, which re-exports this module, comes before the interop rules in the kit's order.
|
|
43
|
+
_QUEUE_FULL = "queue-full"
|
|
44
|
+
QUEUE_FULL_MESSAGE = "the agent's queue for this context is full: start again later"
|
|
45
|
+
#: The longest queue a status message names (`queued <position> of <length>`, six digits).
|
|
46
|
+
QUEUE_LENGTH_MAX = 999999
|
|
47
|
+
DEFAULT_RETRY_AFTER_SECONDS = 60
|
|
48
|
+
|
|
49
|
+
_UTC_INSTANT = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-5][0-9](?:\.[0-9]{1,3})?Z", re.ASCII)
|
|
50
|
+
_QUEUED = re.compile(r"queued ([1-9][0-9]{0,5}) of ([1-9][0-9]{0,5})", re.ASCII)
|
|
51
|
+
_ORIGIN_PATH = re.compile("/(?!/)[^%s]*" % re.escape(_JS_SPACE))
|
|
52
|
+
_CONTEXT = re.compile(r"(?:personal|org:[A-Za-z0-9][A-Za-z0-9_-]{0,127})", re.ASCII)
|
|
53
|
+
_KEY_PATH = "/extensions/" + LOAD_EXTENSION_KEY.replace("~", "~0").replace("/", "~1")
|
|
54
|
+
_LIMITS = (("concurrency", 1, 1024), ("perCaller", 1, 1024), ("queueMax", 0, 100000))
|
|
55
|
+
_ROW_COUNTS = ("queued", "running", "completed", "failed")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _utc_ms(value: Any) -> float:
|
|
59
|
+
"""A UTC instant (`T`, `Z`, at most three fraction digits, a day that exists) in milliseconds, else NaN."""
|
|
60
|
+
return parse_rfc3339_ms(value) if isinstance(value, str) and _UTC_INSTANT.fullmatch(value) else math.nan
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _strict_ms(value: Any) -> float:
|
|
64
|
+
return parse_rfc3339_ms(value) if isinstance(value, str) else math.nan
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _utf16(value: str) -> bytes:
|
|
68
|
+
"""A key that orders strings as JavaScript's `<` does: by UTF-16 code units."""
|
|
69
|
+
return value.encode("utf-16-be", "surrogatepass")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _string(value: Any) -> bool:
|
|
73
|
+
return isinstance(value, str)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# --- The call queue (fabric-interop, DEC-0104) ---
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def job_caller_key(accepted: Any = MISSING, credential: Any = MISSING) -> Optional[str]:
|
|
80
|
+
"""The key of a job's caller at a node that queues (DEC-0104), as the contract's jobCallerKey: for a call whose
|
|
81
|
+
delegation the node accepted (verify_delegation's answer), its `principal` and `caller` and the partner
|
|
82
|
+
organization when the chain ends in one (DEC-0090); for a call with no token, the credential the node knows the
|
|
83
|
+
caller by (a gate's pass with `principal: None` is that case too). A refused verdict has no key, and neither has
|
|
84
|
+
an unreadable call: None. The key is opaque; only equality matters."""
|
|
85
|
+
if isinstance(accepted, dict) and not (get(accepted, "ok") is True and get(accepted, "principal") is None):
|
|
86
|
+
principal, caller = get(accepted, "principal"), get(accepted, "caller")
|
|
87
|
+
if get(accepted, "ok") is not True or not _string(principal) or principal == "" or not _string(caller) or caller == "":
|
|
88
|
+
return None
|
|
89
|
+
chain = get(accepted, "chain")
|
|
90
|
+
chain = chain if isinstance(chain, list) else []
|
|
91
|
+
last = chain[-1] if chain else MISSING
|
|
92
|
+
partner = [last] if len(chain) > 1 and _string(last) and last.startswith("org:") else []
|
|
93
|
+
return json_stringify(["delegated", principal, caller] + partner)
|
|
94
|
+
return json_stringify(["credential", credential]) if _string(credential) and credential != "" else None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _limits_ok(limits: Any) -> bool:
|
|
98
|
+
if not isinstance(limits, dict):
|
|
99
|
+
return False
|
|
100
|
+
concurrency, per_caller, queue_max = get(limits, "concurrency"), get(limits, "perCaller"), get(limits, "queueMax")
|
|
101
|
+
return (all(safe_integer(n) and double(n) >= 1 for n in (concurrency, per_caller))
|
|
102
|
+
and safe_integer(queue_max) and double(queue_max) >= 0)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _waiting_job(job: Any) -> bool:
|
|
106
|
+
return isinstance(job, dict) and _string(get(job, "id")) and _string(get(job, "caller")) and math.isfinite(_utc_ms(get(job, "at")))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def next_queued_job(queue: Any, running: Any, limits: Any) -> Optional[str]:
|
|
110
|
+
"""The job the hosting node starts next for one (agentRef, context), or None (DEC-0104). Nothing starts while
|
|
111
|
+
`concurrency` jobs run (every running job holds a slot, its caller readable or not). Of the waiting jobs whose
|
|
112
|
+
caller runs fewer than `perCaller`, the one whose caller runs the fewest wins; between those callers, the one whose
|
|
113
|
+
oldest waiting job arrived first; within a caller, its oldest. Arrival ties break by id (UTF-16 order)."""
|
|
114
|
+
if not _limits_ok(limits):
|
|
115
|
+
return None
|
|
116
|
+
if (len(running) if isinstance(running, list) else 0) >= double(limits["concurrency"]):
|
|
117
|
+
return None
|
|
118
|
+
running_of: Dict[str, int] = {}
|
|
119
|
+
for job in objects(running):
|
|
120
|
+
if _string(get(job, "caller")):
|
|
121
|
+
running_of[job["caller"]] = running_of.get(job["caller"], 0) + 1
|
|
122
|
+
waiting = [job for job in objects(queue) if _waiting_job(job) and running_of.get(job["caller"], 0) < double(limits["perCaller"])]
|
|
123
|
+
waiting.sort(key=lambda job: (_utc_ms(job["at"]), _utf16(job["id"])))
|
|
124
|
+
best: Optional[Dict[str, Any]] = None
|
|
125
|
+
for job in waiting:
|
|
126
|
+
if best is None or running_of.get(job["caller"], 0) < running_of.get(best["caller"], 0):
|
|
127
|
+
best = job
|
|
128
|
+
return None if best is None else best["id"]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def admit_job(queue: Any, running: Any, job: Any, limits: Any, retry_after_seconds: Any = MISSING) -> Dict[str, Any]:
|
|
132
|
+
"""Whether a start is admitted (DEC-0104): it is when the job would start at once (next_queued_job over the queue
|
|
133
|
+
with it added answers its id), or when fewer than `queueMax` jobs wait; otherwise {admit: False, error} with the
|
|
134
|
+
`queue-full` refusal and its `retryAfterSeconds` (1 to 3600, else 60). Unreadable limits or job refuse."""
|
|
135
|
+
waiting = objects(queue)
|
|
136
|
+
if _limits_ok(limits) and _waiting_job(job) and (
|
|
137
|
+
next_queued_job(waiting + [job], running, limits) == job["id"] or len(waiting) < double(limits["queueMax"])):
|
|
138
|
+
return {"admit": True}
|
|
139
|
+
after = retry_after_seconds if safe_integer(retry_after_seconds) and 1 <= double(retry_after_seconds) <= 3600 else DEFAULT_RETRY_AFTER_SECONDS
|
|
140
|
+
return {"admit": False, "error": {"code": _QUEUE_FULL, "message": QUEUE_FULL_MESSAGE,
|
|
141
|
+
"data": {"retryable": True, "retryAfterSeconds": int(after)}}}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def queue_status_message(position: Any, length: Any) -> Optional[str]:
|
|
145
|
+
"""The `statusMessage` of a waiting job (DEC-0104): `queued <position> of <length>`, 1 is next, 1 <= position <=
|
|
146
|
+
length <= 999999; None for anything else."""
|
|
147
|
+
if not (safe_integer(position) and safe_integer(length)):
|
|
148
|
+
return None
|
|
149
|
+
if not 1 <= double(position) <= double(length) <= QUEUE_LENGTH_MAX:
|
|
150
|
+
return None
|
|
151
|
+
return "queued %d of %d" % (int(position), int(length))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def read_queue_position(status_message: Any) -> Optional[Dict[str, int]]:
|
|
155
|
+
"""A waiting job's place, read from its `statusMessage`, or None when it is not the queue's grammar."""
|
|
156
|
+
match = _QUEUED.fullmatch(status_message) if _string(status_message) else None
|
|
157
|
+
if not match:
|
|
158
|
+
return None
|
|
159
|
+
position, length = int(match.group(1)), int(match.group(2))
|
|
160
|
+
return {"position": position, "length": length} if 1 <= position <= length else None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _finding(code: str, instance_path: str, message: str) -> Dict[str, str]:
|
|
164
|
+
return {"code": code, "instancePath": instance_path, "message": message}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def job_queue_rules(value: Any) -> List[Dict[str, str]]:
|
|
168
|
+
"""FAC-SEM-131 on {queue, running, limits, started} (`started`: the id the node started, or None): it is
|
|
169
|
+
next_queued_job's."""
|
|
170
|
+
want = next_queued_job(get(value, "queue"), get(value, "running"), get(value, "limits"))
|
|
171
|
+
started = get(value, "started")
|
|
172
|
+
if same(None if started is MISSING else started, want):
|
|
173
|
+
return []
|
|
174
|
+
return [_finding("FAC-SEM-131", "/started", "nothing may start now" if want is None else "the job to start is %s" % show(want))]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# --- The load report (DEC-0104) ---
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def load_report_rules(report: Any) -> List[Dict[str, str]]:
|
|
181
|
+
"""FAC-SEM-132 on a `service-load/1` report: windows in order one `windowSeconds` apart, none after `generatedAt`,
|
|
182
|
+
one row per context, p50 at most p95, a run time only where a job ended."""
|
|
183
|
+
findings: List[Dict[str, str]] = []
|
|
184
|
+
flag = lambda path, message: findings.append(_finding("FAC-SEM-132", path, message)) # noqa: E731
|
|
185
|
+
seconds = get(report, "windowSeconds")
|
|
186
|
+
step = double(seconds) * 1000 if safe_integer(seconds) else math.nan
|
|
187
|
+
generated = _strict_ms(get(report, "generatedAt"))
|
|
188
|
+
previous: Any = MISSING
|
|
189
|
+
for index, window in enumerate(objects(get(report, "windows"))):
|
|
190
|
+
start = _strict_ms(get(window, "start"))
|
|
191
|
+
if start > generated:
|
|
192
|
+
flag("/windows/%d/start" % index, "a window starts after the report was generated")
|
|
193
|
+
if previous is not MISSING and start - previous != step:
|
|
194
|
+
flag("/windows/%d/start" % index, "windows are not in order one windowSeconds apart")
|
|
195
|
+
previous = start
|
|
196
|
+
seen = set()
|
|
197
|
+
for at, row in enumerate(objects(get(window, "byContext"))):
|
|
198
|
+
path = "/windows/%d/byContext/%d" % (index, at)
|
|
199
|
+
context = get(row, "context")
|
|
200
|
+
if _string(context) and context in seen:
|
|
201
|
+
flag(path + "/context", "context %s appears twice in one window" % show(context))
|
|
202
|
+
if _string(context):
|
|
203
|
+
seen.add(context)
|
|
204
|
+
for name in ("waitMs", "runMs"):
|
|
205
|
+
pct = get(row, name)
|
|
206
|
+
p50, p95 = get(pct, "p50"), get(pct, "p95")
|
|
207
|
+
if isinstance(pct, dict) and is_number(p50) and is_number(p95) and double(p50) > double(p95):
|
|
208
|
+
flag("%s/%s" % (path, name), "p50 is above p95")
|
|
209
|
+
if isinstance(get(row, "runMs"), dict) and same(get(row, "completed"), 0) and same(get(row, "failed"), 0):
|
|
210
|
+
flag(path + "/runMs", "a run time with no job ended in the window")
|
|
211
|
+
return findings
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _integer(value: Any, low: float, high: Optional[float], at: str, errors: List[str]) -> None:
|
|
215
|
+
"""JSON Schema's integer with its bounds, as ajv reads it: a number with no fraction (2.0 is one), never a bool."""
|
|
216
|
+
if not is_number(value) or not math.isfinite(double(value)) or double(value) != math.floor(double(value)):
|
|
217
|
+
errors.append("%s type" % at)
|
|
218
|
+
elif double(value) < low:
|
|
219
|
+
errors.append("%s minimum" % at)
|
|
220
|
+
elif high is not None and double(value) > high:
|
|
221
|
+
errors.append("%s maximum" % at)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _members(value: Dict[str, Any], allowed: tuple, required: tuple, at: str, errors: List[str]) -> None:
|
|
225
|
+
errors.extend("%s/%s additionalProperties" % (at, key) for key in value if key not in allowed)
|
|
226
|
+
errors.extend("%s required %s" % (at or "/", key) for key in required if key not in value)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _timestamp(value: Any, at: str, errors: List[str]) -> None:
|
|
230
|
+
if not _string(value):
|
|
231
|
+
errors.append("%s type" % at)
|
|
232
|
+
elif not date_time(value):
|
|
233
|
+
errors.append("%s format" % at)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _percentiles(value: Any, at: str, errors: List[str]) -> None:
|
|
237
|
+
if not isinstance(value, dict):
|
|
238
|
+
errors.append("%s type" % at)
|
|
239
|
+
return
|
|
240
|
+
_members(value, ("p50", "p95"), ("p50", "p95"), at, errors)
|
|
241
|
+
for name in ("p50", "p95"):
|
|
242
|
+
if name in value:
|
|
243
|
+
_integer(value[name], 0, None, "%s/%s" % (at, name), errors)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _row_problems(row: Any, at: str, errors: List[str]) -> None:
|
|
247
|
+
if not isinstance(row, dict):
|
|
248
|
+
errors.append("%s type" % at)
|
|
249
|
+
return
|
|
250
|
+
_members(row, ("context",) + _ROW_COUNTS + ("waitMs", "runMs"), ("context",) + _ROW_COUNTS, at, errors)
|
|
251
|
+
if "context" in row and not (_string(row["context"]) and _CONTEXT.fullmatch(row["context"])):
|
|
252
|
+
errors.append("%s/context %s" % (at, "pattern" if _string(row["context"]) else "type"))
|
|
253
|
+
for name in _ROW_COUNTS:
|
|
254
|
+
if name in row:
|
|
255
|
+
_integer(row[name], 0, None, "%s/%s" % (at, name), errors)
|
|
256
|
+
for name in ("waitMs", "runMs"):
|
|
257
|
+
if name in row:
|
|
258
|
+
_percentiles(row[name], "%s/%s" % (at, name), errors)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def load_report_problems(report: Any) -> List[str]:
|
|
262
|
+
"""service-load.schema.json: what makes `report` not a `service-load/1` report, each as `<path> <keyword>`; empty
|
|
263
|
+
for a valid one. Counts and durations only: a member the schema does not name (a caller, a job id) is refused."""
|
|
264
|
+
if not isinstance(report, dict):
|
|
265
|
+
return ["/ type"]
|
|
266
|
+
errors: List[str] = []
|
|
267
|
+
_members(report, ("type", "generatedAt", "windowSeconds", "windows"), ("type", "generatedAt", "windowSeconds", "windows"), "", errors)
|
|
268
|
+
if "type" in report and not same(report["type"], LOAD_REPORT_TYPE):
|
|
269
|
+
errors.append("/type const")
|
|
270
|
+
if "generatedAt" in report:
|
|
271
|
+
_timestamp(report["generatedAt"], "/generatedAt", errors)
|
|
272
|
+
if "windowSeconds" in report:
|
|
273
|
+
_integer(report["windowSeconds"], 60, 86400, "/windowSeconds", errors)
|
|
274
|
+
windows = report.get("windows", MISSING)
|
|
275
|
+
if windows is not MISSING and not isinstance(windows, list):
|
|
276
|
+
errors.append("/windows type")
|
|
277
|
+
elif isinstance(windows, list):
|
|
278
|
+
if len(windows) > 168:
|
|
279
|
+
errors.append("/windows maxItems")
|
|
280
|
+
for index, window in enumerate(windows):
|
|
281
|
+
at = "/windows/%d" % index
|
|
282
|
+
if not isinstance(window, dict):
|
|
283
|
+
errors.append("%s type" % at)
|
|
284
|
+
continue
|
|
285
|
+
_members(window, ("start", "byContext"), ("start", "byContext"), at, errors)
|
|
286
|
+
if "start" in window:
|
|
287
|
+
_timestamp(window["start"], at + "/start", errors)
|
|
288
|
+
rows = window.get("byContext", MISSING)
|
|
289
|
+
if rows is not MISSING and not isinstance(rows, list):
|
|
290
|
+
errors.append("%s/byContext type" % at)
|
|
291
|
+
elif isinstance(rows, list):
|
|
292
|
+
if len(rows) > 64:
|
|
293
|
+
errors.append("%s/byContext maxItems" % at)
|
|
294
|
+
for row_index, row in enumerate(rows):
|
|
295
|
+
_row_problems(row, "%s/byContext/%d" % (at, row_index), errors)
|
|
296
|
+
return errors
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _declaration_problems(value: Any) -> List[str]:
|
|
300
|
+
"""service-load-extension.schema.json, each problem as `<path> <keyword>`; empty for a valid declaration."""
|
|
301
|
+
if not isinstance(value, dict):
|
|
302
|
+
return ["/ type"]
|
|
303
|
+
errors: List[str] = []
|
|
304
|
+
_members(value, ("path", "windowSeconds", "limits"), ("path", "windowSeconds", "limits"), "", errors)
|
|
305
|
+
path = value.get("path", MISSING)
|
|
306
|
+
if path is not MISSING:
|
|
307
|
+
if not _string(path):
|
|
308
|
+
errors.append("/path type")
|
|
309
|
+
else:
|
|
310
|
+
if len(path) > 256: # code points, as ajv counts them; the originPath's own 2048 is wider
|
|
311
|
+
errors.append("/path maxLength")
|
|
312
|
+
if not _ORIGIN_PATH.fullmatch(path):
|
|
313
|
+
errors.append("/path pattern")
|
|
314
|
+
if "windowSeconds" in value:
|
|
315
|
+
_integer(value["windowSeconds"], 60, 86400, "/windowSeconds", errors)
|
|
316
|
+
limits = value.get("limits", MISSING)
|
|
317
|
+
if limits is not MISSING:
|
|
318
|
+
if not isinstance(limits, dict):
|
|
319
|
+
errors.append("/limits type")
|
|
320
|
+
else:
|
|
321
|
+
_members(limits, tuple(name for name, _, _ in _LIMITS), tuple(name for name, _, _ in _LIMITS), "/limits", errors)
|
|
322
|
+
for name, low, high in _LIMITS:
|
|
323
|
+
if name in limits:
|
|
324
|
+
_integer(limits[name], low, high, "/limits/%s" % name, errors)
|
|
325
|
+
return errors
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def load_view(well_known: Any) -> Dict[str, Any]:
|
|
329
|
+
"""The load declaration as a reader reads it (DEC-0104): `declared` when the extension key is present, `valid` when
|
|
330
|
+
its value is the schema's, `load` the declaration when valid (else None) and `errors` its problems. A reader treats
|
|
331
|
+
an invalid value as not declared and reports it (FAC-SEM-133)."""
|
|
332
|
+
extensions = get(well_known, "extensions")
|
|
333
|
+
if not isinstance(extensions, dict) or LOAD_EXTENSION_KEY not in extensions:
|
|
334
|
+
return {"declared": False, "valid": False, "load": None, "errors": []}
|
|
335
|
+
value = extensions[LOAD_EXTENSION_KEY]
|
|
336
|
+
errors = _declaration_problems(value)
|
|
337
|
+
return {"declared": True, "valid": not errors, "load": None if errors else value, "errors": errors}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def load_extension_rules(well_known: Any) -> List[Dict[str, str]]:
|
|
341
|
+
"""FAC-SEM-133 on a well-known document: a value under the load extension key is valid against
|
|
342
|
+
service-load-extension.schema.json; one finding at the key otherwise."""
|
|
343
|
+
view = load_view(well_known)
|
|
344
|
+
if not view["declared"] or view["valid"]:
|
|
345
|
+
return []
|
|
346
|
+
return [_finding("FAC-SEM-133", _KEY_PATH, "the load declaration is invalid (%s); a reader treats it as not declared "
|
|
347
|
+
"and reports it (DEC-0104)" % "; ".join(view["errors"]))]
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def load_extension(path: Any, window_seconds: Any, limits: Any) -> Dict[str, Any]:
|
|
351
|
+
"""The `extensions` entry a service declares (DEC-0104): where it serves its `service-load/1` report (an origin
|
|
352
|
+
path, served under the descriptor token like the usage report), the report's window and the queue's limits per
|
|
353
|
+
(agentRef, context). Raises ServiceError on a declaration a reader would treat as not declared."""
|
|
354
|
+
value = {"path": path, "windowSeconds": window_seconds, "limits": dict(limits) if isinstance(limits, dict) else limits}
|
|
355
|
+
problems = _declaration_problems(value)
|
|
356
|
+
if problems:
|
|
357
|
+
raise ServiceError("Load declaration: %s." % "; ".join(problems))
|
|
358
|
+
return {LOAD_EXTENSION_KEY: value}
|
|
@@ -73,8 +73,42 @@ class InstanceLock:
|
|
|
73
73
|
#: a file is allowed there).
|
|
74
74
|
WINDOWS_LOCK_OFFSET = 1 << 30
|
|
75
75
|
|
|
76
|
+
#: How many times acquire re-opens service.lock when the file it locked is no longer the file at its path (a
|
|
77
|
+
#: folder swapped under it by a data import), before it gives up with an error naming the path (FAA-293).
|
|
78
|
+
INODE_RETRIES = 3
|
|
79
|
+
|
|
76
80
|
def acquire(self) -> "InstanceLock":
|
|
77
81
|
ensure_private_dir(self.path.parent)
|
|
82
|
+
for _ in range(self.INODE_RETRIES):
|
|
83
|
+
if self._acquire_once():
|
|
84
|
+
return self
|
|
85
|
+
raise ServiceError("%s kept moving while it was locked (a folder swapped under the data folder?): no copy "
|
|
86
|
+
"holds a file that is no longer at its path; start again once the folder is settled." % self.path)
|
|
87
|
+
|
|
88
|
+
def _locked_file_is_at_path(self, fd: int) -> bool:
|
|
89
|
+
"""The inode this copy locked is the file at the path now (FAA-293). Windows does not rename a folder holding
|
|
90
|
+
an open file, and its lock is a byte range on this handle, so the check is POSIX only."""
|
|
91
|
+
if WINDOWS:
|
|
92
|
+
return True
|
|
93
|
+
try:
|
|
94
|
+
here, held = os.stat(str(self.path)), os.fstat(fd)
|
|
95
|
+
except FileNotFoundError:
|
|
96
|
+
return False
|
|
97
|
+
return (here.st_dev, here.st_ino) == (held.st_dev, held.st_ino)
|
|
98
|
+
|
|
99
|
+
def _let_go(self, fd: int) -> None:
|
|
100
|
+
try:
|
|
101
|
+
os.close(fd)
|
|
102
|
+
except OSError:
|
|
103
|
+
pass
|
|
104
|
+
name, self._name = self._name, None
|
|
105
|
+
if name is not None:
|
|
106
|
+
try:
|
|
107
|
+
_drop_lock_name(name)
|
|
108
|
+
except OSError:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def _acquire_once(self) -> bool:
|
|
78
112
|
fd = os.open(str(self.path), os.O_RDWR | os.O_CREAT | _O_BINARY, 0o600)
|
|
79
113
|
try:
|
|
80
114
|
if WINDOWS:
|
|
@@ -92,6 +126,9 @@ class InstanceLock:
|
|
|
92
126
|
if exc.errno in (errno.EWOULDBLOCK, errno.EAGAIN, errno.EACCES, getattr(errno, "EDEADLOCK", -1)):
|
|
93
127
|
raise AlreadyRunning(_read_pid(self.path), self.path) from None
|
|
94
128
|
raise
|
|
129
|
+
if not self._locked_file_is_at_path(fd): # another copy may hold the file at the path: re-open that one
|
|
130
|
+
os.close(fd)
|
|
131
|
+
return False
|
|
95
132
|
try:
|
|
96
133
|
self._name = _hold_lock_name(self.path)
|
|
97
134
|
except BaseException:
|
|
@@ -114,8 +151,11 @@ class InstanceLock:
|
|
|
114
151
|
except OSError: # as with the close: nothing to add to the first error
|
|
115
152
|
pass
|
|
116
153
|
raise
|
|
154
|
+
if not self._locked_file_is_at_path(fd): # swapped while the pid was written: this copy holds nothing real
|
|
155
|
+
self._let_go(fd)
|
|
156
|
+
return False
|
|
117
157
|
self._fd = fd
|
|
118
|
-
return
|
|
158
|
+
return True
|
|
119
159
|
|
|
120
160
|
def release(self) -> None:
|
|
121
161
|
if self._fd is not None:
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py
CHANGED
|
@@ -366,6 +366,10 @@ class Client:
|
|
|
366
366
|
if answer is not None:
|
|
367
367
|
answer["attempts"] = attempts
|
|
368
368
|
return answer
|
|
369
|
+
if outcome == "foreign-user": # FAA-257: the fallback would go to the same refused listener
|
|
370
|
+
raise OpenRouterError("The base URL's port is held by another OS account's process, or its owner "
|
|
371
|
+
"cannot be told; nothing was sent (DEC-0154 HA-2)", kind="foreign-user",
|
|
372
|
+
attempts=attempts)
|
|
369
373
|
if outcome in _STOP_AT_ONCE.values():
|
|
370
374
|
raise OpenRouterError("OpenRouter refused the key or the request (%s)" % status, kind=outcome,
|
|
371
375
|
attempts=attempts)
|
|
@@ -390,6 +394,8 @@ class Client:
|
|
|
390
394
|
# the provider may have billed a call whose answer never arrived whole (a cut connection looks the
|
|
391
395
|
# same as a refused one from here): counted, never as $0; over-counting is the safe side
|
|
392
396
|
self.ledger.record(self._unmeasured(asked, task_run_id, worst))
|
|
397
|
+
if err.kind == "foreign-user": # nothing was sent: nothing billed, and no other model there (FAA-257)
|
|
398
|
+
return "foreign-user", err.status, None, None
|
|
393
399
|
return ("retry" if err.kind in ("connection", "timeout") else "refused"), err.status, None, None
|
|
394
400
|
status = response.status
|
|
395
401
|
if status in _STOP_AT_ONCE:
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""peer_owner: which OS account owns each end of a loopback TCP connection.
|
|
2
2
|
|
|
3
|
-
peer_owner 1.1.0+kit.
|
|
3
|
+
peer_owner 1.1.0+kit.4: Observatory's peer_owner 1.1.0, vendored into the Fabric service kit as `fabric_peer_owner` (layer L0;
|
|
4
4
|
DEC-0154 HA-2, FAA-226) from Project Observatory's `observatory/engine/peer_owner.py` at
|
|
5
5
|
commit f86f0622 (passioncode-ai/project-observatory-dashboard, the same licence). It differs
|
|
6
6
|
from that file in this paragraph, in `Owner` and `Row` (NamedTuples here where it has
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
macOS: the sysctl calibration also requires a child process's listener, an empty netstat reads as
|
|
20
20
|
`TABLE_RESTRICTED`, and a caller may vouch for a listener with a pid it started (`pid_listens`,
|
|
21
21
|
`parse_lsof_listeners`, `LSOF`, `CheckedHTTPConnection(vouch_pid=...)`, `own_connection(vouch_pid=...)`); since kit.3
|
|
22
|
-
the child's port is waited on with `selectors` and read under one deadline (FAA-273).
|
|
22
|
+
the child's port is waited on with `selectors` and read under one deadline (FAA-273); since kit.4 a number is ASCII
|
|
23
|
+
digits only and a netstat with only its header reads as `TABLE_RESTRICTED` (FAA-273 items 3 and 4).
|
|
23
24
|
A limit /proc sets: it carries a socket's creator and no pid, so a listening socket that root
|
|
24
25
|
created and handed to another account (systemd socket activation with User=) reads as root's. Standard library only, no import of another kit module.
|
|
25
26
|
The kit's tests are `test/test_fabric_peer_owner.py`, over `test/fixtures/peer-owner/`;
|
|
@@ -110,7 +111,7 @@ import time
|
|
|
110
111
|
from typing import NamedTuple, Optional, Tuple
|
|
111
112
|
|
|
112
113
|
# This file's own version, for projects that copy it; not the engine's (that is configuration.VERSION).
|
|
113
|
-
PEER_OWNER_VERSION = "1.1.0+kit.
|
|
114
|
+
PEER_OWNER_VERSION = "1.1.0+kit.4" # Observatory's 1.1.0 with the kit's differences above
|
|
114
115
|
|
|
115
116
|
THIS_ACCOUNT = "this-account"
|
|
116
117
|
ANOTHER_ACCOUNT = "another-account"
|
|
@@ -231,6 +232,11 @@ def classify(uid: Optional[int], pid: Optional[int], *, method: str, own_uid: Op
|
|
|
231
232
|
|
|
232
233
|
# -- parsers: pure functions over captured text or bytes (the tests' fixtures) ---------------------------------------
|
|
233
234
|
|
|
235
|
+
def _ascii_digits(text: str) -> bool:
|
|
236
|
+
"""`[0-9]+` as the Node twin reads a number; `str.isdigit` also takes `٤` and `²` (FAA-273 item 4)."""
|
|
237
|
+
return text != "" and text.isascii() and text.isdigit()
|
|
238
|
+
|
|
239
|
+
|
|
234
240
|
def parse_netstat(text: str) -> list[Row]:
|
|
235
241
|
"""`netstat -anv -p tcp` on macOS. Process names may hold spaces ("Some App:123"), so the
|
|
236
242
|
`process:pid` column is what is left between the leading and the trailing columns, both
|
|
@@ -246,6 +252,10 @@ def parse_netstat(text: str) -> list[Row]:
|
|
|
246
252
|
# "Local Address" and "Foreign Address" are two header words each and one data column each.
|
|
247
253
|
leading = at - sum(1 for w in names[:at] if w == "Address")
|
|
248
254
|
trailing = len(names) - at - 1
|
|
255
|
+
if not any(line.split()[:1] and line.split()[0].startswith("tcp") for line in lines[header + 1:]):
|
|
256
|
+
# The caller's own connection is in any table it can see: a header and no tcp row is the restricted table
|
|
257
|
+
# (FAA-273 item 3), not `no-row`.
|
|
258
|
+
raise TableUnreadable(f"{TABLE_RESTRICTED}: netstat printed only its header to this process")
|
|
249
259
|
rows: list[Row] = []
|
|
250
260
|
for line in lines[header + 1:]:
|
|
251
261
|
tok = line.split()
|
|
@@ -259,7 +269,7 @@ def parse_netstat(text: str) -> list[Row]:
|
|
|
259
269
|
pid = None
|
|
260
270
|
if ":" in process:
|
|
261
271
|
tail = process.rsplit(":", 1)[1]
|
|
262
|
-
pid = int(tail) if tail
|
|
272
|
+
pid = int(tail) if _ascii_digits(tail) else None
|
|
263
273
|
rows.append(Row(local, remote, tok[5] == "LISTEN", None, pid, tok[5] == "TIME_WAIT"))
|
|
264
274
|
return rows
|
|
265
275
|
|
|
@@ -283,7 +293,7 @@ def parse_lsof_listeners(text: str) -> list[tuple[str, int]]:
|
|
|
283
293
|
if not line.startswith("n") or ":" not in line:
|
|
284
294
|
continue
|
|
285
295
|
host, port = line[1:].rsplit(":", 1)
|
|
286
|
-
if port
|
|
296
|
+
if _ascii_digits(port):
|
|
287
297
|
found.append((norm_host(host.strip("[]")), int(port)))
|
|
288
298
|
return found
|
|
289
299
|
|
|
@@ -291,7 +301,7 @@ def parse_lsof_listeners(text: str) -> list[tuple[str, int]]:
|
|
|
291
301
|
def parse_ps_uid(text: str) -> Optional[int]:
|
|
292
302
|
"""`ps -o uid= -p PID`: one number, or nothing when the process is gone."""
|
|
293
303
|
text = text.strip()
|
|
294
|
-
return int(text) if text
|
|
304
|
+
return int(text) if _ascii_digits(text) else None
|
|
295
305
|
|
|
296
306
|
|
|
297
307
|
def parse_proc_net_tcp(text: str, *, family: int, byteorder: Optional[str] = None) -> list[Row]:
|
|
@@ -540,7 +550,7 @@ def _child_listener_seen(rows_of) -> Optional[bool]:
|
|
|
540
550
|
port = None
|
|
541
551
|
try:
|
|
542
552
|
line = _child_line(child, deadline)
|
|
543
|
-
if line is None or not line.strip()
|
|
553
|
+
if line is None or not _ascii_digits(line.strip()):
|
|
544
554
|
return None
|
|
545
555
|
port = int(line.strip())
|
|
546
556
|
try:
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py
CHANGED
|
@@ -6,6 +6,7 @@ only. The dashboard kit's fabric-button.js carries its own copy of the name and
|
|
|
6
6
|
|
|
7
7
|
It reads as the contract's ECMAScript does (FAA-136): ECMAScript's whitespace (Python's `\\s` also takes U+0085 and
|
|
8
8
|
U+001C to U+001F, and not U+FEFF), `.` stopping at every line terminator, ASCII case folding, an ASCII word boundary."""
|
|
9
|
+
# Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
|
|
9
10
|
|
|
10
11
|
from __future__ import annotations
|
|
11
12
|
|
|
@@ -142,7 +143,7 @@ def _percent_encode(text: str, also: str) -> str:
|
|
|
142
143
|
return "".join(out)
|
|
143
144
|
|
|
144
145
|
|
|
145
|
-
def
|
|
146
|
+
def url_parts(value: str) -> Tuple[str, str, str]:
|
|
146
147
|
"""The password, query and fragment of an absolute URL as WHATWG URL parsing splits them, read by hand: urlsplit
|
|
147
148
|
refuses a `[` or `]` outside an IPv6 host (`<https>://u:p[w@h/`), where `new URL` reads the password (0.56.0 unit
|
|
148
149
|
r7). The fragment starts at the first `#`, the query at the first `?` before it. A special scheme's authority starts
|
|
@@ -166,6 +167,9 @@ def _url_parts(value: str) -> Tuple[str, str, str]:
|
|
|
166
167
|
_percent_encode(fragment, "`"))
|
|
167
168
|
|
|
168
169
|
|
|
170
|
+
_url_parts = url_parts # the name before FAA-250, kept for code that reached it
|
|
171
|
+
|
|
172
|
+
|
|
169
173
|
def secret_url(value: str) -> bool:
|
|
170
174
|
"""Whether a URL an MCP entry or a launch names carries a credential (LC-21): anything secret_argument finds, a
|
|
171
175
|
password in its user information, or a query or fragment parameter whose name can only carry a credential and has
|
|
@@ -177,7 +181,7 @@ def secret_url(value: str) -> bool:
|
|
|
177
181
|
value = re.sub(r"[\t\n\r]", "", value.strip(_C0_OR_SPACE))
|
|
178
182
|
if not _SCHEME.match(value):
|
|
179
183
|
return False # not an absolute URL (WHATWG URL parsing refuses it)
|
|
180
|
-
password, query, fragment =
|
|
184
|
+
password, query, fragment = url_parts(value)
|
|
181
185
|
if password:
|
|
182
186
|
return True
|
|
183
187
|
for part in (query, fragment):
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py
CHANGED
|
@@ -22,6 +22,7 @@ from fabric_time import DAY_MS as _DAY_MS, utc_date_ms as _utc_date_ms # noqa:
|
|
|
22
22
|
from fabric_base import ( # noqa: F401
|
|
23
23
|
PROTOCOL,
|
|
24
24
|
EXTENSION_KEY,
|
|
25
|
+
PRIMARY_CAPABILITY,
|
|
25
26
|
SUPERVISOR_ENV,
|
|
26
27
|
DRAIN_SECONDS,
|
|
27
28
|
_ID,
|
|
@@ -196,6 +197,24 @@ from fabric_connections import ( # noqa: F401
|
|
|
196
197
|
connections_rules,
|
|
197
198
|
connections_view,
|
|
198
199
|
)
|
|
200
|
+
from fabric_load import ( # noqa: F401 - the call queue and the load report (DEC-0104, FAA-289)
|
|
201
|
+
LOAD_EXTENSION_KEY,
|
|
202
|
+
LOAD_REPORT_TYPE,
|
|
203
|
+
QUEUE_FULL_MESSAGE,
|
|
204
|
+
QUEUE_LENGTH_MAX,
|
|
205
|
+
DEFAULT_RETRY_AFTER_SECONDS,
|
|
206
|
+
job_caller_key,
|
|
207
|
+
next_queued_job,
|
|
208
|
+
admit_job,
|
|
209
|
+
queue_status_message,
|
|
210
|
+
read_queue_position,
|
|
211
|
+
job_queue_rules,
|
|
212
|
+
load_report_rules,
|
|
213
|
+
load_report_problems,
|
|
214
|
+
load_view,
|
|
215
|
+
load_extension_rules,
|
|
216
|
+
load_extension,
|
|
217
|
+
)
|
|
199
218
|
from fabric_descriptor import ( # noqa: F401
|
|
200
219
|
DASHBOARD_EXTENSION_KEY,
|
|
201
220
|
DASHBOARD_KIT,
|
|
@@ -230,7 +249,9 @@ from fabric_descriptor import ( # noqa: F401
|
|
|
230
249
|
fabric_button_presence,
|
|
231
250
|
placement_of,
|
|
232
251
|
remote_origin_problems,
|
|
252
|
+
port_listener_refusal,
|
|
233
253
|
port_of,
|
|
254
|
+
port_owner,
|
|
234
255
|
NO_SOURCE_REPOSITORY,
|
|
235
256
|
NoSourceRepositoryWarning,
|
|
236
257
|
names_repository,
|
|
@@ -319,6 +340,9 @@ from fabric_supervisors import ( # noqa: F401
|
|
|
319
340
|
launchd_loaded,
|
|
320
341
|
launchd_pid,
|
|
321
342
|
fetch_well_known,
|
|
343
|
+
well_known_of,
|
|
344
|
+
job_well_known_of,
|
|
345
|
+
ForeignUserAnswer,
|
|
322
346
|
_OVERRIDE_ROW,
|
|
323
347
|
launchd_override,
|
|
324
348
|
launchd_install,
|