@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.
Files changed (105) hide show
  1. package/CHANGELOG.md +256 -0
  2. package/README.md +14 -7
  3. package/bin/fabric-agent-adapter.js +26 -4
  4. package/package.json +1 -1
  5. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  6. package/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +6 -4
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +4 -4
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +42 -7
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +13 -3
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +3 -3
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +12 -4
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +23 -8
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +6 -2
  15. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +2 -1
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +4 -2
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +6 -3
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +34 -8
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/README.md +67 -0
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/dashboard_service.py +252 -0
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.mjs +168 -0
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.py +179 -0
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/README.md +57 -0
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.mjs +181 -0
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.py +237 -0
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +4 -4
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +11 -6
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +1 -1
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +42 -7
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +6 -2
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +12 -1
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +26 -3
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +4 -2
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +14 -4
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +6 -2
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +44 -3
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +12 -4
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +7 -5
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +10 -5
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +1 -0
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +5 -0
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +2 -0
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +15 -6
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +3 -2
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +37 -5
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +30 -2
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +48 -5
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +9 -3
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -2
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +344 -0
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +54 -20
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +4 -0
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +8 -2
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +4 -1
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +8 -0
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +55 -14
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +8 -1
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +6 -0
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +3 -0
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +16 -7
  64. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +4 -2
  65. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +97 -3
  66. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +50 -8
  67. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +22 -4
  68. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +49 -3
  69. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +9 -2
  70. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +15 -2
  71. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +2 -2
  72. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +358 -0
  73. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +41 -1
  74. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +6 -0
  75. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +17 -7
  76. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +6 -2
  77. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +24 -0
  78. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +110 -16
  79. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +12 -1
  80. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +543 -37
  81. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +4 -1
  82. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +12 -5
  83. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +4 -4
  84. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +42 -7
  85. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +6 -3
  86. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +13 -3
  87. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +13 -4
  88. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +32 -0
  89. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/working-rules.md +54 -0
  90. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +12 -4
  91. package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
  92. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +27 -26
  93. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +4 -4
  94. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/cleanup.md +30 -0
  95. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +59 -38
  96. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +52 -26
  97. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/remote-sessions.md +75 -0
  98. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +28 -13
  99. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/task-context.md +58 -0
  100. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/waits.md +36 -0
  101. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +14 -0
  102. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +169 -46
  103. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +146 -19
  104. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +78 -3
  105. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +9 -2
@@ -4,6 +4,7 @@ removed without touching what the operator turned off; release pruning (LC-15).
4
4
  from __future__ import annotations
5
5
 
6
6
  import http.client
7
+ import ipaddress
7
8
  import json
8
9
  import os
9
10
  import plistlib
@@ -140,12 +141,40 @@ def _check(result: Any, what: str) -> None:
140
141
  raise ServiceError("%s failed: %s" % (what, (getattr(result, "stderr", "") or getattr(result, "stdout", "") or "").strip()[:200]))
141
142
 
142
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
+
143
167
  def systemd_install(unit: str, text: str, *, unit_dir: Optional[Path] = None, run: Callable[..., Any] = subprocess.run,
144
- system: str = sys.platform, force_enable: bool = False) -> Path:
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:
145
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.
146
173
 
147
174
  The operator's intent wins (LC-14): a unit already installed and disabled stays off — the file is rewritten, so
148
- 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``."""
149
178
  if not system.startswith("linux"):
150
179
  raise ServiceError("systemd is the supervisor on Linux only")
151
180
  if not _UNIT.match(unit):
@@ -162,6 +191,8 @@ def systemd_install(unit: str, text: str, *, unit_dir: Optional[Path] = None, ru
162
191
  return path
163
192
  _check(run(["systemctl", "--user", "enable", unit], capture_output=True, text=True), "systemctl --user enable")
164
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)
165
196
  return path
166
197
 
167
198
 
@@ -226,9 +257,13 @@ _TASK_DISABLED = re.compile(r"<Settings>(?:(?!</Settings>).)*<Enabled>false</Ena
226
257
 
227
258
 
228
259
  def task_install(task: str, xml_text: str, *, run: Callable[..., Any] = subprocess.run, system: str = sys.platform,
229
- force_enable: bool = False) -> None:
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:
230
263
  """Create (or replace) the task from its XML without administrator rights, then (re)start it so the new
231
- 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."""
232
267
  if system != "win32":
233
268
  raise ServiceError("Task Scheduler is the supervisor on Windows only")
234
269
  query = run(["schtasks", "/Query", "/TN", task, "/XML"], capture_output=True, text=True)
@@ -247,6 +282,9 @@ def task_install(task: str, xml_text: str, *, run: Callable[..., Any] = subproce
247
282
  if existed:
248
283
  run(["schtasks", "/End", "/TN", task], capture_output=True, text=True)
249
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
250
288
 
251
289
 
252
290
  def task_uninstall(task: str, *, run: Callable[..., Any] = subprocess.run, system: str = sys.platform) -> None:
@@ -367,13 +405,16 @@ def launchd_loaded(label: str, domain: Optional[str] = None) -> bool:
367
405
  return _launchctl("print", "%s/%s" % (launchd_domain(domain=domain), label)).returncode == 0
368
406
 
369
407
 
370
- _PID_ROW = re.compile(r"^\s*pid\s*=\s*(\d+)\s*\Z")
408
+ _PID_ROW = re.compile(r"^\s*pid\s*=\s*([0-9]+)\s*\Z") # ASCII digits, as Node reads them (FAA-273)
371
409
 
372
410
 
373
411
  def launchd_pid(label: str, domain: Optional[str] = None) -> Optional[int]:
374
412
  """The running pid of this account's launchd job ``label`` (with ``domain="system"``, of the LaunchDaemon), or
375
413
  None (not running, or not readable). Reading needs no root."""
376
- result = _launchctl("print", "%s/%s" % (launchd_domain(domain=domain), label))
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)
377
418
  if result.returncode != 0:
378
419
  return None
379
420
  for line in (result.stdout or "").splitlines():
@@ -383,6 +424,29 @@ def launchd_pid(label: str, domain: Optional[str] = None) -> Optional[int]:
383
424
  return None
384
425
 
385
426
 
427
+ def _loopback_host(host: Optional[str]) -> bool:
428
+ """Whether `host` (urlsplit's hostname: lower-case, no brackets) names this machine: localhost, 127/8 or ::1."""
429
+ if not host:
430
+ return False
431
+ if host == "localhost":
432
+ return True
433
+ try:
434
+ return ipaddress.ip_address(host).is_loopback
435
+ except ValueError:
436
+ return False
437
+
438
+
439
+ class ForeignUserAnswer:
440
+ """A refused listener (DEC-0154 HA-2), as `well_known_of` answers it: never a document, whatever keys a service's JSON
441
+ carries. `reason` starts with `foreign-user` or `table-restricted` (the Node twin's class, FAA-259)."""
442
+
443
+ def __init__(self, reason: str) -> None:
444
+ self.reason = reason
445
+
446
+ def __repr__(self) -> str:
447
+ return "ForeignUserAnswer(%r)" % self.reason
448
+
449
+
386
450
  def _read_well_known(origin: str, timeout: float = 2.0,
387
451
  vouch_pid: Optional[int] = None) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
388
452
  """(the well-known answer or None, why the listener was refused or None). A loopback origin is read only on a
@@ -391,10 +455,16 @@ def _read_well_known(origin: str, timeout: float = 2.0,
391
455
  table hides other processes. `vouch_pid`, the pid of a launchd job this
392
456
  caller started, proves the listener when the socket table hides other processes (FAA-267)."""
393
457
  parts = urllib.parse.urlsplit(origin)
394
- if parts.scheme == "http" and parts.hostname == "127.0.0.1" and parts.port:
395
- conn = _po.CheckedHTTPConnection("127.0.0.1", parts.port, timeout=timeout, vouch_pid=vouch_pid)
458
+ try:
459
+ port = parts.port or (80 if parts.scheme == "http" else None) # no port is http's 80, as in the Node twin
460
+ except ValueError: # a port out of range
461
+ port = None
462
+ if parts.scheme == "http" and port and _loopback_host(parts.hostname):
463
+ # every loopback spelling (127.0.0.1, another 127/8 address, localhost, [::1]) goes on a checked connection,
464
+ # never the unchecked way (FAA-257)
465
+ conn = _po.CheckedHTTPConnection(parts.hostname, port, timeout=timeout, vouch_pid=vouch_pid)
396
466
  try:
397
- conn.request("GET", "/.well-known/fabric-service", headers={"Host": "127.0.0.1:%d" % parts.port})
467
+ conn.request("GET", "/.well-known/fabric-service", headers={"Host": parts.netloc.rpartition("@")[2]})
398
468
  response = conn.getresponse()
399
469
  return (json.loads(response.read().decode()) if response.status == 200 else None), None
400
470
  except _po.ListenerRefused as exc:
@@ -514,10 +584,24 @@ def _read_job_well_known(origin: str, job_pid: int, job_uid: Optional[int] = Non
514
584
 
515
585
  def fetch_well_known(origin: str, timeout: float = 2.0) -> Optional[Dict[str, Any]]:
516
586
  """The well-known answer at `origin`, or None: nothing answers, the answer is not JSON, or the listener is not this
517
- OS account's process (DEC-0154 HA-2)."""
587
+ OS account's process (DEC-0154 HA-2). `well_known_of` tells the last two apart."""
518
588
  return _read_well_known(origin, timeout)[0]
519
589
 
520
590
 
591
+ def well_known_of(origin: str, timeout: float = 2.0, vouch_pid: Optional[int] = None) -> Any:
592
+ """The well-known answer at `origin` as the Node `wellKnownOf` gives it (FAA-259): the document; None when nothing
593
+ answers or the answer is not JSON (down); a `ForeignUserAnswer` when the listener is another OS account's process or
594
+ its owner cannot be told. So a status reads HA-2's order: down, foreign-user, foreign, status."""
595
+ doc, why = _read_well_known(origin, timeout, vouch_pid=vouch_pid)
596
+ return ForeignUserAnswer(why) if why else doc
597
+
598
+
599
+ def job_well_known_of(origin: str, job_pid: int, job_uid: Optional[int] = None, timeout: float = 2.0) -> Any:
600
+ """As `well_known_of`, for a LaunchDaemon whose job `job_pid` (running as `job_uid`) must hold the listener."""
601
+ doc, why = _read_job_well_known(origin, job_pid, job_uid)
602
+ return ForeignUserAnswer(why) if why else doc
603
+
604
+
521
605
  _OVERRIDE_ROW = re.compile(r'^\s*"([^"]+)"\s*=>\s*(\S+)\s*\Z')
522
606
 
523
607
 
@@ -593,7 +677,9 @@ def _refuse_held_label(label: str, plist_path: Path) -> None:
593
677
 
594
678
  def launchd_install(label: str, plist_path: Path, plist_bytes: bytes, *, origin: str,
595
679
  service_id: str, instance: str = "default", timeout: float = 40.0,
596
- force_enable: bool = False, domain: Optional[str] = None) -> Dict[str, Any]:
680
+ force_enable: bool = False, domain: Optional[str] = None,
681
+ fetch_well_known: Optional[Callable[..., Any]] = None,
682
+ fetch_job_well_known: Optional[Callable[..., Any]] = None) -> Dict[str, Any]:
597
683
  """Write + lint the plist, bootout and WAIT for unload, bootstrap (retry EIO 5), then
598
684
  poll the well-known document until it answers with this identity.
599
685
 
@@ -606,7 +692,13 @@ def launchd_install(label: str, plist_path: Path, plist_bytes: bytes, *, origin:
606
692
  A plist in ``/Library/LaunchDaemons`` (or ``domain="system"``) is a LaunchDaemon in launchd's system domain, started
607
693
  at boot with no login (FAA-278): only root installs it, refused before anything is written otherwise, and the
608
694
  answer counts only when launchd's own job accepted the connection (its ``UserName``, not the installer, owns it).
609
- A ``gui/<uid>`` bootstrap with no GUI session (SSH, a headless Mac after a FileVault restart) says so."""
695
+ A ``gui/<uid>`` bootstrap with no GUI session (SSH, a headless Mac after a FileVault restart) says so.
696
+
697
+ ``fetch_well_known(origin, vouch_pid=...)`` and ``fetch_job_well_known(origin, job_pid, job_uid)`` read the answer as
698
+ ``well_known_of`` and ``job_well_known_of`` do (the document, None, or a ``ForeignUserAnswer``); a caller may inject
699
+ its own, as the Node ``launchdInstall`` takes ``fetchWellKnown`` (FAA-259)."""
700
+ read_answer = fetch_well_known or (lambda origin_, vouch_pid=None: well_known_of(origin_, vouch_pid=vouch_pid))
701
+ read_job = fetch_job_well_known or job_well_known_of
610
702
  plist_path = Path(plist_path)
611
703
  dom = launchd_domain(plist_path, domain)
612
704
  which = "system" if dom == "system" else None
@@ -685,9 +777,10 @@ def launchd_install(label: str, plist_path: Path, plist_bytes: bytes, *, origin:
685
777
  foreign = None
686
778
  while time.time() < deadline:
687
779
  job = launchd_pid(label, "system")
688
- doc, why = _read_job_well_known(origin, job, job_uid) if job else (None, None)
780
+ doc = read_job(origin, job, job_uid) if job else None
781
+ why = doc.reason if isinstance(doc, ForeignUserAnswer) else None
689
782
  foreign = why or foreign # the last refusal stays the reason while launchd shows no pid
690
- if _answers(doc, service_id, instance):
783
+ if not why and _answers(doc, service_id, instance):
691
784
  return doc
692
785
  time.sleep(0.5)
693
786
  loaded = ("; the job stays loaded and its plist starts it again at boot: as root, `launchctl bootout system/%s` "
@@ -702,10 +795,11 @@ def launchd_install(label: str, plist_path: Path, plist_bytes: bytes, *, origin:
702
795
  % (origin, service_id, instance, timeout, loaded))
703
796
  foreign = vouch = None
704
797
  while time.time() < deadline:
705
- doc, foreign = _read_well_known(origin, vouch_pid=vouch)
798
+ doc = read_answer(origin, vouch_pid=vouch)
799
+ foreign = doc.reason if isinstance(doc, ForeignUserAnswer) else None
706
800
  if foreign and foreign.startswith(_po.TABLE_RESTRICTED):
707
801
  vouch = launchd_pid(label) # only a restricted table needs the job's pid (FAA-267)
708
- if _answers(doc, service_id, instance):
802
+ if not foreign and _answers(doc, service_id, instance):
709
803
  return doc
710
804
  time.sleep(0.5)
711
805
  if foreign:
@@ -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 = [_close_totals(run) for _, run in sorted(row.pop("_runs").items())]
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)