@passioncode-ai/fabric-agent-adapter 0.79.1 → 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 +134 -0
- 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/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- 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 +5 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +1 -1
- 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 +9 -1
- 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/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 +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +2 -1
- 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 +8 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +3 -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-board-session.mjs +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +1 -0
- 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-descriptor.mjs +41 -3
- 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-peer-owner.mjs +5 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +38 -9
- 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_board_session.py +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +2 -0
- 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_descriptor.py +35 -2
- 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_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 +20 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +46 -6
- 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 +340 -24
- 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 +3 -1
- 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 +5 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +7 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +28 -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 +21 -21
- 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 +2 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +2 -1
- 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 +143 -36
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +136 -15
- 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
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
|
@@ -197,6 +197,24 @@ from fabric_connections import ( # noqa: F401
|
|
|
197
197
|
connections_rules,
|
|
198
198
|
connections_view,
|
|
199
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
|
+
)
|
|
200
218
|
from fabric_descriptor import ( # noqa: F401
|
|
201
219
|
DASHBOARD_EXTENSION_KEY,
|
|
202
220
|
DASHBOARD_KIT,
|
|
@@ -231,7 +249,9 @@ from fabric_descriptor import ( # noqa: F401
|
|
|
231
249
|
fabric_button_presence,
|
|
232
250
|
placement_of,
|
|
233
251
|
remote_origin_problems,
|
|
252
|
+
port_listener_refusal,
|
|
234
253
|
port_of,
|
|
254
|
+
port_owner,
|
|
235
255
|
NO_SOURCE_REPOSITORY,
|
|
236
256
|
NoSourceRepositoryWarning,
|
|
237
257
|
names_repository,
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py
CHANGED
|
@@ -141,12 +141,40 @@ def _check(result: Any, what: str) -> None:
|
|
|
141
141
|
raise ServiceError("%s failed: %s" % (what, (getattr(result, "stderr", "") or getattr(result, "stdout", "") or "").strip()[:200]))
|
|
142
142
|
|
|
143
143
|
|
|
144
|
+
def _poll_installed(origin: str, service_id: Optional[str], instance: str, timeout: float,
|
|
145
|
+
read: Optional[Callable[[str], Any]], sleep: Callable[[float], None], clock: Callable[[], float]) -> Any:
|
|
146
|
+
"""Poll the well-known answer at `origin` until this OS account's listener answers as `service_id`.`instance`, as
|
|
147
|
+
launchd_install does (the pinned contract's Install row: every supervisor polls, FAA-253). Another account's listener,
|
|
148
|
+
or one whose owner cannot be told, is never a successful install (DEC-0154 HA-2)."""
|
|
149
|
+
if not service_id:
|
|
150
|
+
raise ServiceError("polling the install at %s needs the service_id it must answer as" % origin)
|
|
151
|
+
read = read or well_known_of
|
|
152
|
+
deadline = clock() + max(0.0, timeout)
|
|
153
|
+
while True:
|
|
154
|
+
doc = read(origin)
|
|
155
|
+
foreign = doc.reason if isinstance(doc, ForeignUserAnswer) else None
|
|
156
|
+
if not foreign and _answers(doc, service_id, instance):
|
|
157
|
+
return doc
|
|
158
|
+
if clock() >= deadline:
|
|
159
|
+
break
|
|
160
|
+
sleep(0.5)
|
|
161
|
+
if foreign:
|
|
162
|
+
raise ServiceError("%s is not this service's: %s (DEC-0154 HA-2); the port is another OS account's or its owner "
|
|
163
|
+
"cannot be told." % (origin, foreign))
|
|
164
|
+
raise ServiceError("%s did not answer as %s.%s within %.0f s; read its log." % (origin, service_id, instance, timeout))
|
|
165
|
+
|
|
166
|
+
|
|
144
167
|
def systemd_install(unit: str, text: str, *, unit_dir: Optional[Path] = None, run: Callable[..., Any] = subprocess.run,
|
|
145
|
-
system: str = sys.platform, force_enable: bool = False
|
|
168
|
+
system: str = sys.platform, force_enable: bool = False, origin: Optional[str] = None,
|
|
169
|
+
service_id: Optional[str] = None, instance: str = "default", timeout: float = 40.0,
|
|
170
|
+
fetch_well_known: Optional[Callable[[str], Any]] = None, sleep: Callable[[float], None] = time.sleep,
|
|
171
|
+
clock: Callable[[], float] = time.monotonic) -> Path:
|
|
146
172
|
"""Write the user unit, enable it so it comes back after a login, and (re)start it so the new unit is what runs.
|
|
147
173
|
|
|
148
174
|
The operator's intent wins (LC-14): a unit already installed and disabled stays off — the file is rewritten, so
|
|
149
|
-
the new version is what starts once they enable it — unless ``force_enable`` (the operator asked for it on).
|
|
175
|
+
the new version is what starts once they enable it — unless ``force_enable`` (the operator asked for it on).
|
|
176
|
+
With ``origin`` (and ``service_id``), it then polls the well-known answer as ``launchd_install`` does, until this
|
|
177
|
+
account's listener answers within ``timeout`` (FAA-253); ``fetch_well_known(origin)`` reads it as ``well_known_of``."""
|
|
150
178
|
if not system.startswith("linux"):
|
|
151
179
|
raise ServiceError("systemd is the supervisor on Linux only")
|
|
152
180
|
if not _UNIT.match(unit):
|
|
@@ -163,6 +191,8 @@ def systemd_install(unit: str, text: str, *, unit_dir: Optional[Path] = None, ru
|
|
|
163
191
|
return path
|
|
164
192
|
_check(run(["systemctl", "--user", "enable", unit], capture_output=True, text=True), "systemctl --user enable")
|
|
165
193
|
_check(run(["systemctl", "--user", "restart", unit], capture_output=True, text=True), "systemctl --user restart")
|
|
194
|
+
if origin is not None:
|
|
195
|
+
_poll_installed(origin, service_id, instance, timeout, fetch_well_known, sleep, clock)
|
|
166
196
|
return path
|
|
167
197
|
|
|
168
198
|
|
|
@@ -227,9 +257,13 @@ _TASK_DISABLED = re.compile(r"<Settings>(?:(?!</Settings>).)*<Enabled>false</Ena
|
|
|
227
257
|
|
|
228
258
|
|
|
229
259
|
def task_install(task: str, xml_text: str, *, run: Callable[..., Any] = subprocess.run, system: str = sys.platform,
|
|
230
|
-
force_enable: bool = False
|
|
260
|
+
force_enable: bool = False, origin: Optional[str] = None, service_id: Optional[str] = None,
|
|
261
|
+
instance: str = "default", timeout: float = 40.0, fetch_well_known: Optional[Callable[[str], Any]] = None,
|
|
262
|
+
sleep: Callable[[float], None] = time.sleep, clock: Callable[[], float] = time.monotonic) -> Any:
|
|
231
263
|
"""Create (or replace) the task from its XML without administrator rights, then (re)start it so the new
|
|
232
|
-
definition is what runs. A task already installed and disabled stays off (LC-14) unless ``force_enable``.
|
|
264
|
+
definition is what runs. A task already installed and disabled stays off (LC-14) unless ``force_enable``.
|
|
265
|
+
With ``origin`` (and ``service_id``), it then polls the well-known answer as ``launchd_install`` does and returns it
|
|
266
|
+
(FAA-253); without, it returns None as before."""
|
|
233
267
|
if system != "win32":
|
|
234
268
|
raise ServiceError("Task Scheduler is the supervisor on Windows only")
|
|
235
269
|
query = run(["schtasks", "/Query", "/TN", task, "/XML"], capture_output=True, text=True)
|
|
@@ -248,6 +282,9 @@ def task_install(task: str, xml_text: str, *, run: Callable[..., Any] = subproce
|
|
|
248
282
|
if existed:
|
|
249
283
|
run(["schtasks", "/End", "/TN", task], capture_output=True, text=True)
|
|
250
284
|
_check(run(["schtasks", "/Run", "/TN", task], capture_output=True, text=True), "schtasks /Run")
|
|
285
|
+
if origin is not None:
|
|
286
|
+
return _poll_installed(origin, service_id, instance, timeout, fetch_well_known, sleep, clock)
|
|
287
|
+
return None
|
|
251
288
|
|
|
252
289
|
|
|
253
290
|
def task_uninstall(task: str, *, run: Callable[..., Any] = subprocess.run, system: str = sys.platform) -> None:
|
|
@@ -368,13 +405,16 @@ def launchd_loaded(label: str, domain: Optional[str] = None) -> bool:
|
|
|
368
405
|
return _launchctl("print", "%s/%s" % (launchd_domain(domain=domain), label)).returncode == 0
|
|
369
406
|
|
|
370
407
|
|
|
371
|
-
_PID_ROW = re.compile(r"^\s*pid\s*=\s*(
|
|
408
|
+
_PID_ROW = re.compile(r"^\s*pid\s*=\s*([0-9]+)\s*\Z") # ASCII digits, as Node reads them (FAA-273)
|
|
372
409
|
|
|
373
410
|
|
|
374
411
|
def launchd_pid(label: str, domain: Optional[str] = None) -> Optional[int]:
|
|
375
412
|
"""The running pid of this account's launchd job ``label`` (with ``domain="system"``, of the LaunchDaemon), or
|
|
376
413
|
None (not running, or not readable). Reading needs no root."""
|
|
377
|
-
|
|
414
|
+
try:
|
|
415
|
+
result = _launchctl("print", "%s/%s" % (launchd_domain(domain=domain), label))
|
|
416
|
+
except subprocess.TimeoutExpired:
|
|
417
|
+
return None # a launchctl that hangs is a pid not read, never an error out of an install's poll (FAA-273)
|
|
378
418
|
if result.returncode != 0:
|
|
379
419
|
return None
|
|
380
420
|
for line in (result.stdout or "").splitlines():
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py
CHANGED
|
@@ -137,7 +137,7 @@ def _by_project(receipts: List[Dict[str, Any]], first: str, last: str) -> List[D
|
|
|
137
137
|
out = []
|
|
138
138
|
for key in sorted(projects):
|
|
139
139
|
row = projects[key]
|
|
140
|
-
runs = [
|
|
140
|
+
runs = [_close_run(run) for _, run in sorted(row.pop("_runs").items())]
|
|
141
141
|
row = _close_totals(row)
|
|
142
142
|
if runs:
|
|
143
143
|
row["taskRuns"] = runs
|
|
@@ -167,6 +167,17 @@ def _round_usd(value: float) -> float:
|
|
|
167
167
|
return whole / 1e6
|
|
168
168
|
|
|
169
169
|
|
|
170
|
+
def _close_run(run: Dict[str, Any]) -> Dict[str, Any]:
|
|
171
|
+
"""A task run's totals, its cost rounded as `_round_usd` rounds but never above the exact cost: each run and its
|
|
172
|
+
project round apart, so many runs rounding up summed past the project and failed FAC-SEM-025 (board request
|
|
173
|
+
f5753c5a). Rounded this way the runs' sum stays within the project's rounded cost by construction."""
|
|
174
|
+
exact = run["_cost"]
|
|
175
|
+
run = _close_totals(run)
|
|
176
|
+
if run["costUsd"] is not None and run["costUsd"] > exact:
|
|
177
|
+
run["costUsd"] = _round_usd(run["costUsd"] - 0.000001)
|
|
178
|
+
return run
|
|
179
|
+
|
|
180
|
+
|
|
170
181
|
def _close_totals(row: Dict[str, Any]) -> Dict[str, Any]:
|
|
171
182
|
cost = row.pop("_cost")
|
|
172
183
|
row["costUsd"] = None if row["calls"] and row["unpricedCalls"] == row["calls"] else _round_usd(cost)
|