@passioncode-ai/fabric-agent-adapter 0.79.1 → 0.81.0
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 +213 -0
- package/README.md +13 -2
- package/SECURITY.md +23 -8
- package/bin/fabric-agent-adapter.js +50 -5
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +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 +12 -4
- 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/references/source-and-instance.md +44 -7
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +107 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +26 -6
- 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 +24 -5
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +57 -76
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_restart.py +56 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +912 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/new_agent.py +308 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +8 -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/recipes/support-agent/README.md +1 -1
- 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 +12 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +7 -3
- 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 +54 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +6 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +33 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -7
- 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_layout.mjs +21 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +24 -33
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +12 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +136 -3
- 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 +118 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +266 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +72 -7
- 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-package.mjs +98 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +7 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +10 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +11 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +114 -20
- 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 +120 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +316 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +40 -3
- 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_package.py +103 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +28 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +24 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +23 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +110 -9
- 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 +469 -29
- 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 +18 -6
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +44 -0
- 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/communication-agent-template.md +19 -328
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +12 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +1 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/new-agent.md +42 -0
- 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/source-and-instance.md +44 -7
- 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/schemas/template/access.schema.json +50 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/connections.schema.json +23 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/data-rooms.schema.json +74 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/manifest.schema.json +170 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/template-header.schema.json +25 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/template_check.py +491 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/CHANGELOG.md +7 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/TEMPLATE.md +347 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/access.json +24 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/assembly.md +15 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/checks/acceptance.md +17 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/connections.json +29 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/data-rooms.json +9 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/manifest.json +32 -0
- 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 +3 -2
- 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_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
|
|
|
@@ -29,8 +30,20 @@ CREDENTIAL_PAIRS: Dict[str, tuple] = {
|
|
|
29
30
|
JS_SPACE = "\t\n\x0b\x0c\r \xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
|
|
30
31
|
_NOT_LINE_END = "[^\n\r\u2028\u2029]" # ECMAScript's `.` without the s flag
|
|
31
32
|
JWT_SHAPE = re.compile(r"eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*")
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
#: Where a provider key's prefix may start (DEC-0151): the start of a value, a character that is no ASCII letter or
|
|
34
|
+
#: digit, or an escape (a percent-escape, single or double, a backslash escape, an octal, `\\x` with two hex digits,
|
|
35
|
+
#: `\\u` with four, an ESC colour sequence, raw or escaped). A consuming group, as the contract writes it.
|
|
36
|
+
_KEY_BOUNDARY = (r"(?:^|[^A-Za-z0-9]|%[0-9A-Fa-f]{2}|\\[nrt\"']|\\u[0-9A-Fa-f]{4}|\\x[0-9A-Fa-f]{2}|\\[bfv0ae]|\\[0-7]{1,3}"
|
|
37
|
+
r"|%25[0-9A-Fa-f]{2}|\x1b\[[0-9;:]*m|(?:\\u001[bB]|\\x1[bB]|\\e|\\033|\\33)\[[0-9;:]*m)")
|
|
38
|
+
#: The provider keys' shapes, prefixes in their canonical case only (DEC-0151 item 7: `SK-` or `GHP_` is no key). An
|
|
39
|
+
#: `sk-` key holds, within 256 characters, a digit and an upper-case letter with 16 or more characters, or a digit with
|
|
40
|
+
#: 32 or more; the look-aheads are bounded to 255 characters and lazy, so a long run of prefixes reads in linear time.
|
|
41
|
+
_PROVIDER_KEYS = (r"(?:sk-(?=[A-Za-z0-9_-]{0,255}?[0-9])(?:(?=[A-Za-z0-9_-]{0,255}?[A-Z])[A-Za-z0-9_-]{16,}|[A-Za-z0-9_-]{32,})"
|
|
42
|
+
r"|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,})")
|
|
43
|
+
#: The contract's SECRET_VALUE at the pin (DEC-0151): `Bearer ` at a value's start in any case, or a provider key after a
|
|
44
|
+
#: boundary. No case-insensitive flag; digits are ASCII. A match includes its boundary character: callers test, never
|
|
45
|
+
#: read the matched text or its index.
|
|
46
|
+
SECRET_VALUE = re.compile("(^[Bb][Ee][Aa][Rr][Ee][Rr][" + JS_SPACE + "]" + _NOT_LINE_END + "+|" + _KEY_BOUNDARY + _PROVIDER_KEYS + ")")
|
|
34
47
|
_LONG_RUN = re.compile(r"[A-Za-z0-9+/=_-]{40,}")
|
|
35
48
|
_BEARER_INSIDE = re.compile(r"(?<![A-Za-z0-9_])bearer[%s]+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{12,}=*" % JS_SPACE,
|
|
36
49
|
re.IGNORECASE | re.ASCII)
|
|
@@ -142,7 +155,7 @@ def _percent_encode(text: str, also: str) -> str:
|
|
|
142
155
|
return "".join(out)
|
|
143
156
|
|
|
144
157
|
|
|
145
|
-
def
|
|
158
|
+
def url_parts(value: str) -> Tuple[str, str, str]:
|
|
146
159
|
"""The password, query and fragment of an absolute URL as WHATWG URL parsing splits them, read by hand: urlsplit
|
|
147
160
|
refuses a `[` or `]` outside an IPv6 host (`<https>://u:p[w@h/`), where `new URL` reads the password (0.56.0 unit
|
|
148
161
|
r7). The fragment starts at the first `#`, the query at the first `?` before it. A special scheme's authority starts
|
|
@@ -166,6 +179,9 @@ def _url_parts(value: str) -> Tuple[str, str, str]:
|
|
|
166
179
|
_percent_encode(fragment, "`"))
|
|
167
180
|
|
|
168
181
|
|
|
182
|
+
_url_parts = url_parts # the name before FAA-250, kept for code that reached it
|
|
183
|
+
|
|
184
|
+
|
|
169
185
|
def secret_url(value: str) -> bool:
|
|
170
186
|
"""Whether a URL an MCP entry or a launch names carries a credential (LC-21): anything secret_argument finds, a
|
|
171
187
|
password in its user information, or a query or fragment parameter whose name can only carry a credential and has
|
|
@@ -177,7 +193,7 @@ def secret_url(value: str) -> bool:
|
|
|
177
193
|
value = re.sub(r"[\t\n\r]", "", value.strip(_C0_OR_SPACE))
|
|
178
194
|
if not _SCHEME.match(value):
|
|
179
195
|
return False # not an absolute URL (WHATWG URL parsing refuses it)
|
|
180
|
-
password, query, fragment =
|
|
196
|
+
password, query, fragment = url_parts(value)
|
|
181
197
|
if password:
|
|
182
198
|
return True
|
|
183
199
|
for part in (query, fragment):
|
|
@@ -190,7 +206,10 @@ def secret_url(value: str) -> bool:
|
|
|
190
206
|
_REPOSITORY_URI = re.compile(r"\A([A-Za-z][A-Za-z0-9+.-]*:(?://[^/?#]*)?)([^?#]*)(.*)\Z", re.S)
|
|
191
207
|
_REPOSITORY_NAME = re.compile(r"(?:[a-z0-9._-]+|[A-Za-z._-]+)\Z") # lower case, or letters with no digit (readings 8)
|
|
192
208
|
_REPOSITORY_AUTHORITY = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://")
|
|
193
|
-
|
|
209
|
+
#: A repository name's own key shapes, boundary-free and case-sensitive (DEC-0151): SECRET_VALUE takes a consuming
|
|
210
|
+
#: boundary, so composed after a separator look-behind it would miss a key after `-`, `.` or `_`.
|
|
211
|
+
_REPOSITORY_KEYS = r"sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,}"
|
|
212
|
+
_KEY_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))(?:%s)" % _REPOSITORY_KEYS)
|
|
194
213
|
_JWT_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))%s" % JWT_SHAPE.pattern)
|
|
195
214
|
|
|
196
215
|
|
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,
|
|
@@ -210,6 +228,8 @@ from fabric_descriptor import ( # noqa: F401
|
|
|
210
228
|
_NOT_LAUNCHD,
|
|
211
229
|
_AUTH_HEADER,
|
|
212
230
|
LoopbackHTTPServer,
|
|
231
|
+
PEER_CHECKS,
|
|
232
|
+
connection_refusal,
|
|
213
233
|
descriptor_path,
|
|
214
234
|
read_descriptors,
|
|
215
235
|
validate_descriptor,
|
|
@@ -231,7 +251,9 @@ from fabric_descriptor import ( # noqa: F401
|
|
|
231
251
|
fabric_button_presence,
|
|
232
252
|
placement_of,
|
|
233
253
|
remote_origin_problems,
|
|
254
|
+
port_listener_refusal,
|
|
234
255
|
port_of,
|
|
256
|
+
port_owner,
|
|
235
257
|
NO_SOURCE_REPOSITORY,
|
|
236
258
|
NoSourceRepositoryWarning,
|
|
237
259
|
names_repository,
|
|
@@ -316,6 +338,7 @@ from fabric_supervisors import ( # noqa: F401
|
|
|
316
338
|
_launchctl,
|
|
317
339
|
_domain,
|
|
318
340
|
SYSTEM_DAEMONS,
|
|
341
|
+
USER_NAME_RULE,
|
|
319
342
|
launchd_domain,
|
|
320
343
|
launchd_loaded,
|
|
321
344
|
launchd_pid,
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py
CHANGED
|
@@ -47,6 +47,8 @@ DEFAULT_EXIT_TIMEOUT = 15
|
|
|
47
47
|
|
|
48
48
|
_LABEL = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,250}\Z") # what launchd and the Node twin take as a label
|
|
49
49
|
_USER_NAME = re.compile(r"^[a-z_][a-z0-9_.-]{0,31}\Z") # a macOS short name, as a LaunchDaemon's UserName takes it
|
|
50
|
+
#: The short-name rule in words, printed alike by both twins (FAA-286 item 13).
|
|
51
|
+
USER_NAME_RULE = "a lower-case letter or _, then up to 31 lower-case letters, digits, _, . or -"
|
|
50
52
|
|
|
51
53
|
#: Where a LaunchDaemon's plist lives: a job in launchd's system domain, started at boot with no GUI login (FAA-278).
|
|
52
54
|
SYSTEM_DAEMONS = Path("/Library/LaunchDaemons")
|
|
@@ -141,12 +143,40 @@ def _check(result: Any, what: str) -> None:
|
|
|
141
143
|
raise ServiceError("%s failed: %s" % (what, (getattr(result, "stderr", "") or getattr(result, "stdout", "") or "").strip()[:200]))
|
|
142
144
|
|
|
143
145
|
|
|
146
|
+
def _poll_installed(origin: str, service_id: Optional[str], instance: str, timeout: float,
|
|
147
|
+
read: Optional[Callable[[str], Any]], sleep: Callable[[float], None], clock: Callable[[], float]) -> Any:
|
|
148
|
+
"""Poll the well-known answer at `origin` until this OS account's listener answers as `service_id`.`instance`, as
|
|
149
|
+
launchd_install does (the pinned contract's Install row: every supervisor polls, FAA-253). Another account's listener,
|
|
150
|
+
or one whose owner cannot be told, is never a successful install (DEC-0154 HA-2)."""
|
|
151
|
+
if not service_id:
|
|
152
|
+
raise ServiceError("polling the install at %s needs the service_id it must answer as" % origin)
|
|
153
|
+
read = read or well_known_of
|
|
154
|
+
deadline = clock() + max(0.0, timeout)
|
|
155
|
+
while True:
|
|
156
|
+
doc = read(origin)
|
|
157
|
+
foreign = doc.reason if isinstance(doc, ForeignUserAnswer) else None
|
|
158
|
+
if not foreign and _answers(doc, service_id, instance):
|
|
159
|
+
return doc
|
|
160
|
+
if clock() >= deadline:
|
|
161
|
+
break
|
|
162
|
+
sleep(0.5)
|
|
163
|
+
if foreign:
|
|
164
|
+
raise ServiceError("%s is not this service's: %s (DEC-0154 HA-2); the port is another OS account's or its owner "
|
|
165
|
+
"cannot be told." % (origin, foreign))
|
|
166
|
+
raise ServiceError("%s did not answer as %s.%s within %.0f s; read its log." % (origin, service_id, instance, timeout))
|
|
167
|
+
|
|
168
|
+
|
|
144
169
|
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
|
|
170
|
+
system: str = sys.platform, force_enable: bool = False, origin: Optional[str] = None,
|
|
171
|
+
service_id: Optional[str] = None, instance: str = "default", timeout: float = 40.0,
|
|
172
|
+
fetch_well_known: Optional[Callable[[str], Any]] = None, sleep: Callable[[float], None] = time.sleep,
|
|
173
|
+
clock: Callable[[], float] = time.monotonic) -> Path:
|
|
146
174
|
"""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
175
|
|
|
148
176
|
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).
|
|
177
|
+
the new version is what starts once they enable it — unless ``force_enable`` (the operator asked for it on).
|
|
178
|
+
With ``origin`` (and ``service_id``), it then polls the well-known answer as ``launchd_install`` does, until this
|
|
179
|
+
account's listener answers within ``timeout`` (FAA-253); ``fetch_well_known(origin)`` reads it as ``well_known_of``."""
|
|
150
180
|
if not system.startswith("linux"):
|
|
151
181
|
raise ServiceError("systemd is the supervisor on Linux only")
|
|
152
182
|
if not _UNIT.match(unit):
|
|
@@ -163,6 +193,8 @@ def systemd_install(unit: str, text: str, *, unit_dir: Optional[Path] = None, ru
|
|
|
163
193
|
return path
|
|
164
194
|
_check(run(["systemctl", "--user", "enable", unit], capture_output=True, text=True), "systemctl --user enable")
|
|
165
195
|
_check(run(["systemctl", "--user", "restart", unit], capture_output=True, text=True), "systemctl --user restart")
|
|
196
|
+
if origin is not None:
|
|
197
|
+
_poll_installed(origin, service_id, instance, timeout, fetch_well_known, sleep, clock)
|
|
166
198
|
return path
|
|
167
199
|
|
|
168
200
|
|
|
@@ -227,9 +259,13 @@ _TASK_DISABLED = re.compile(r"<Settings>(?:(?!</Settings>).)*<Enabled>false</Ena
|
|
|
227
259
|
|
|
228
260
|
|
|
229
261
|
def task_install(task: str, xml_text: str, *, run: Callable[..., Any] = subprocess.run, system: str = sys.platform,
|
|
230
|
-
force_enable: bool = False
|
|
262
|
+
force_enable: bool = False, origin: Optional[str] = None, service_id: Optional[str] = None,
|
|
263
|
+
instance: str = "default", timeout: float = 40.0, fetch_well_known: Optional[Callable[[str], Any]] = None,
|
|
264
|
+
sleep: Callable[[float], None] = time.sleep, clock: Callable[[], float] = time.monotonic) -> Any:
|
|
231
265
|
"""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``.
|
|
266
|
+
definition is what runs. A task already installed and disabled stays off (LC-14) unless ``force_enable``.
|
|
267
|
+
With ``origin`` (and ``service_id``), it then polls the well-known answer as ``launchd_install`` does and returns it
|
|
268
|
+
(FAA-253); without, it returns None as before."""
|
|
233
269
|
if system != "win32":
|
|
234
270
|
raise ServiceError("Task Scheduler is the supervisor on Windows only")
|
|
235
271
|
query = run(["schtasks", "/Query", "/TN", task, "/XML"], capture_output=True, text=True)
|
|
@@ -248,6 +284,9 @@ def task_install(task: str, xml_text: str, *, run: Callable[..., Any] = subproce
|
|
|
248
284
|
if existed:
|
|
249
285
|
run(["schtasks", "/End", "/TN", task], capture_output=True, text=True)
|
|
250
286
|
_check(run(["schtasks", "/Run", "/TN", task], capture_output=True, text=True), "schtasks /Run")
|
|
287
|
+
if origin is not None:
|
|
288
|
+
return _poll_installed(origin, service_id, instance, timeout, fetch_well_known, sleep, clock)
|
|
289
|
+
return None
|
|
251
290
|
|
|
252
291
|
|
|
253
292
|
def task_uninstall(task: str, *, run: Callable[..., Any] = subprocess.run, system: str = sys.platform) -> None:
|
|
@@ -336,6 +375,9 @@ def launchd_domain(plist_path: Optional[Path] = None, domain: Optional[str] = No
|
|
|
336
375
|
session; a ``gui/<uid>`` job starts only while its user is logged in."""
|
|
337
376
|
if domain not in (None, "gui", "system"):
|
|
338
377
|
raise ServiceError("domain must be gui or system")
|
|
378
|
+
if plist_path is not None and re.match(r"^(?:system|gui)(?:/[0-9]*)?\Z", str(plist_path)):
|
|
379
|
+
raise ServiceError("%s is a launchd domain, not a plist path: pass domain=%r (FAA-286)"
|
|
380
|
+
% (plist_path, "system" if str(plist_path) == "system" else "gui"))
|
|
339
381
|
if domain is None and plist_path is not None:
|
|
340
382
|
folder = re.sub(r"^/{2,}", "/", str(Path(os.path.abspath(str(plist_path))).parent)) # POSIX keeps a leading //
|
|
341
383
|
domain = "system" if folder.lower() == str(SYSTEM_DAEMONS).lower() else "gui" # APFS ignores case
|
|
@@ -347,6 +389,39 @@ def launchd_domain(plist_path: Optional[Path] = None, domain: Optional[str] = No
|
|
|
347
389
|
return _domain()
|
|
348
390
|
|
|
349
391
|
|
|
392
|
+
def _in_daemons(plist_path: Path) -> bool:
|
|
393
|
+
folder = re.sub(r"^/{2,}", "/", str(Path(os.path.abspath(str(plist_path))).parent))
|
|
394
|
+
return folder.lower() == str(SYSTEM_DAEMONS).lower()
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _pair_problem(plist_path: Path, domain: Optional[str]) -> Optional[str]:
|
|
398
|
+
"""A domain the plist's folder contradicts (FAA-286 items 14 and 21): a system job in a LaunchAgents folder (launchd
|
|
399
|
+
also loads it into gui/<uid> at login), or a gui job in /Library/LaunchDaemons (launchd loads it at boot as root).
|
|
400
|
+
Another folder is allowed for a system job, as the tests' and a staging install's are."""
|
|
401
|
+
if domain == "system" and Path(os.path.abspath(str(plist_path))).parent.name.lower() == "launchagents":
|
|
402
|
+
return ("A LaunchDaemon's plist lives in %s: %s is in a LaunchAgents folder, which launchd also loads into "
|
|
403
|
+
"gui/<uid> at login, so the job would run twice." % (SYSTEM_DAEMONS, plist_path))
|
|
404
|
+
if domain == "gui" and _in_daemons(plist_path):
|
|
405
|
+
return ("%s is in %s, which launchd loads at boot as a LaunchDaemon: a gui/<uid> job's plist goes in "
|
|
406
|
+
"~/Library/LaunchAgents." % (plist_path, SYSTEM_DAEMONS))
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _leftover_agent(label: str, user_name: str) -> Optional[Path]:
|
|
411
|
+
"""A LaunchAgent of ``label`` in ``user_name``'s ~/Library/LaunchAgents, which would run a second copy at its login
|
|
412
|
+
(FAA-286 item 6)."""
|
|
413
|
+
try:
|
|
414
|
+
import pwd
|
|
415
|
+
home = Path(pwd.getpwnam(user_name).pw_dir)
|
|
416
|
+
except (ImportError, KeyError, ValueError):
|
|
417
|
+
return None
|
|
418
|
+
try:
|
|
419
|
+
agents = sorted((home / "Library" / "LaunchAgents").glob("*.plist"))
|
|
420
|
+
except OSError:
|
|
421
|
+
return None
|
|
422
|
+
return next((p for p in agents if _plist_field(p, "Label") == label), None)
|
|
423
|
+
|
|
424
|
+
|
|
350
425
|
def _need_root(domain: str, label: str, plist_path: Path, install: bool) -> None:
|
|
351
426
|
"""A system-domain job is installed and removed by root: refuse before anything is written (FAA-278)."""
|
|
352
427
|
if domain != "system" or os.geteuid() == 0:
|
|
@@ -368,13 +443,16 @@ def launchd_loaded(label: str, domain: Optional[str] = None) -> bool:
|
|
|
368
443
|
return _launchctl("print", "%s/%s" % (launchd_domain(domain=domain), label)).returncode == 0
|
|
369
444
|
|
|
370
445
|
|
|
371
|
-
_PID_ROW = re.compile(r"^\s*pid\s*=\s*(
|
|
446
|
+
_PID_ROW = re.compile(r"^\s*pid\s*=\s*([0-9]+)\s*\Z") # ASCII digits, as Node reads them (FAA-273)
|
|
372
447
|
|
|
373
448
|
|
|
374
449
|
def launchd_pid(label: str, domain: Optional[str] = None) -> Optional[int]:
|
|
375
450
|
"""The running pid of this account's launchd job ``label`` (with ``domain="system"``, of the LaunchDaemon), or
|
|
376
451
|
None (not running, or not readable). Reading needs no root."""
|
|
377
|
-
|
|
452
|
+
try:
|
|
453
|
+
result = _launchctl("print", "%s/%s" % (launchd_domain(domain=domain), label))
|
|
454
|
+
except subprocess.TimeoutExpired:
|
|
455
|
+
return None # a launchctl that hangs is a pid not read, never an error out of an install's poll (FAA-273)
|
|
378
456
|
if result.returncode != 0:
|
|
379
457
|
return None
|
|
380
458
|
for line in (result.stdout or "").splitlines():
|
|
@@ -660,6 +738,9 @@ def launchd_install(label: str, plist_path: Path, plist_bytes: bytes, *, origin:
|
|
|
660
738
|
read_answer = fetch_well_known or (lambda origin_, vouch_pid=None: well_known_of(origin_, vouch_pid=vouch_pid))
|
|
661
739
|
read_job = fetch_job_well_known or job_well_known_of
|
|
662
740
|
plist_path = Path(plist_path)
|
|
741
|
+
pair = _pair_problem(plist_path, domain)
|
|
742
|
+
if pair:
|
|
743
|
+
raise ServiceError(pair)
|
|
663
744
|
dom = launchd_domain(plist_path, domain)
|
|
664
745
|
which = "system" if dom == "system" else None
|
|
665
746
|
_need_root(dom, label, plist_path, True)
|
|
@@ -673,8 +754,8 @@ def launchd_install(label: str, plist_path: Path, plist_bytes: bytes, *, origin:
|
|
|
673
754
|
"the job's pid and uid (FAA-278); %s is not." % origin)
|
|
674
755
|
try:
|
|
675
756
|
named = plistlib.loads(plist_bytes).get("UserName")
|
|
676
|
-
except Exception: #
|
|
677
|
-
|
|
757
|
+
except Exception as exc: # named as what it is, never as a missing UserName (FAA-286 item 16)
|
|
758
|
+
raise ServiceError("The plist does not parse (%s): write it with launchd_plist." % type(exc).__name__)
|
|
678
759
|
named = named if isinstance(named, str) else None # a UserName that is not a string names no account
|
|
679
760
|
if named:
|
|
680
761
|
_refuse_held_label(label, plist_path)
|
|
@@ -691,7 +772,7 @@ def launchd_install(label: str, plist_path: Path, plist_bytes: bytes, *, origin:
|
|
|
691
772
|
"it), or uninstall that one as root first." % (plist_path, installed))
|
|
692
773
|
if named and not _USER_NAME.match(named):
|
|
693
774
|
raise ServiceError("UserName %s is not a macOS short name (%s): launchd takes the account's short name."
|
|
694
|
-
% (named,
|
|
775
|
+
% (named, USER_NAME_RULE))
|
|
695
776
|
job_uid = _account_uid(named) if named else None
|
|
696
777
|
if named and job_uid is None:
|
|
697
778
|
raise ServiceError("UserName %s is no account of this Mac: launchd would not start the job. Name the account "
|
|
@@ -704,6 +785,12 @@ def launchd_install(label: str, plist_path: Path, plist_bytes: bytes, *, origin:
|
|
|
704
785
|
raise ServiceError("A LaunchDaemon needs UserName: without it the job runs as root, keeps its data in root's "
|
|
705
786
|
"home, and every client refuses root's listener (DEC-0154 HA-2). Write the plist with "
|
|
706
787
|
"launchd_plist(..., user_name=<the account whose Fabric and agents call it>).")
|
|
788
|
+
leftover = _leftover_agent(label, named)
|
|
789
|
+
if leftover is not None:
|
|
790
|
+
raise ServiceError("%s is a LaunchAgent of label %s: it would start a second copy at %s's login beside this "
|
|
791
|
+
"LaunchDaemon. Remove it first (launchd_uninstall as %s; lifecycle reference, The system "
|
|
792
|
+
"domain)." % (leftover, label, named, named))
|
|
793
|
+
existed = plist_path.exists()
|
|
707
794
|
atomic_write(plist_path, plist_bytes, 0o644)
|
|
708
795
|
lint = subprocess.run(["plutil", "-lint", str(plist_path)], capture_output=True, text=True)
|
|
709
796
|
if lint.returncode != 0:
|
|
@@ -731,6 +818,16 @@ def launchd_install(label: str, plist_path: Path, plist_bytes: bytes, *, origin:
|
|
|
731
818
|
"headless Mac after a FileVault restart), so %s does not run jobs: remove the LaunchAgent "
|
|
732
819
|
"this attempt wrote (launchd_uninstall), then install it as a LaunchDaemon in the system "
|
|
733
820
|
"domain (a plist in %s with UserName, as root; FAA-278)." % (last, dom, SYSTEM_DAEMONS))
|
|
821
|
+
if dom == "system":
|
|
822
|
+
# a plist left in /Library/LaunchDaemons loads at the next boot (FAA-286 item 15): one this call wrote goes
|
|
823
|
+
if not existed:
|
|
824
|
+
try:
|
|
825
|
+
plist_path.unlink()
|
|
826
|
+
except OSError:
|
|
827
|
+
pass
|
|
828
|
+
raise ServiceError("launchctl bootstrap failed: %s; %s" % (last, "the plist this call wrote was removed"
|
|
829
|
+
if not plist_path.exists() else "%s stays and loads at the next boot: remove it as root"
|
|
830
|
+
% plist_path))
|
|
734
831
|
raise ServiceError("launchctl bootstrap failed: %s" % last)
|
|
735
832
|
deadline = time.time() + timeout
|
|
736
833
|
if dom == "system":
|
|
@@ -794,6 +891,10 @@ def launchd_uninstall(label: str, plist_path: Path, *, timeout: float = 30.0, pu
|
|
|
794
891
|
"remove the data as that user.")
|
|
795
892
|
if dom == "system":
|
|
796
893
|
_refuse_held_label(label, Path(plist_path)) # a bootout would stop another plist's job (FAA-278)
|
|
894
|
+
holds = _plist_field(Path(plist_path), "Label") if Path(plist_path).exists() else None
|
|
895
|
+
if holds is not None and holds != label: # FAA-286 item 9: booting out one job and unlinking another's plist
|
|
896
|
+
raise ServiceError("%s holds launchd label %s, not %s: nothing was removed. Name the plist of %s, or uninstall %s "
|
|
897
|
+
"with its own label." % (plist_path, holds, label, label, holds))
|
|
797
898
|
dirs = instance_dirs(str(service_id), instance) if purge else {}
|
|
798
899
|
target = "%s/%s" % (dom, label)
|
|
799
900
|
_launchctl("bootout", target)
|
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)
|