@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
@@ -133,12 +133,20 @@ def check(data):
133
133
  return {'ok': True, 'kind': kind}
134
134
 
135
135
 
136
+ def read_input(limit=65536):
137
+ # The handoff's bytes, decoded as UTF-8 with a byte order mark dropped: Windows before Python 3.15 decodes a text
138
+ # stdin with its code page, which misreads a Cyrillic handoff (FAA-210). A stream without bytes is read as text.
139
+ stream = getattr(sys.stdin, 'buffer', None)
140
+ raw = stream.read(limit + 1) if stream is not None else sys.stdin.read(limit + 1)
141
+ require(len(raw) <= limit, 'input_too_large')
142
+ return raw.decode('utf-8-sig') if isinstance(raw, bytes) else raw.lstrip('\ufeff')
143
+
144
+
136
145
  def main():
137
146
  try:
138
- raw = sys.stdin.read(65537)
139
- require(len(raw) <= 65536, 'input_too_large')
140
- result = check(json.loads(raw))
141
- except (ValueError, TypeError, KeyError, AttributeError):
147
+ result = check(json.loads(read_input()))
148
+ # RecursionError: Python 3.9 to 3.13 raise it on a deeply nested document under the cap
149
+ except (ValueError, TypeError, KeyError, AttributeError, RecursionError):
142
150
  # Fixed reasons only: never echo a submitted URL, token or JSON blob.
143
151
  result = {'ok': False, 'reason': 'dashboard_handoff_refused'}
144
152
  print(json.dumps(result))
@@ -17,6 +17,7 @@ itself takes an option of the same name. --env is for a stdio server only and is
17
17
 
18
18
  Normative source: fabric-agent-contract docs/specification/provider.md (DEC-0016, rulings DEC-0017).
19
19
  """
20
+ # Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
20
21
 
21
22
  # #region provider-writer — docs: plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md#the-writer
22
23
 
@@ -61,8 +62,12 @@ _URL = re.compile(URL_PATTERN)
61
62
  _MANIFEST = re.compile(MANIFEST_PATTERN[:-1] + r"\Z")
62
63
  # The manifest's name after a backslash: valid since DEC-0145, but no writer emits it until every reader accepts it.
63
64
  _MANIFEST_AFTER_BACKSLASH = re.compile(r"\\fabric-agent\.json\Z")
64
- # format: uri (RFC 3986 absolute URI): a scheme, a colon, then no whitespace or control character.
65
- _URI = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:[^\s\x00-\x1f\x7f]+\Z")
65
+ # format: uri (RFC 3986 absolute URI) as ajv-formats reads it: RFC 3986 characters only (unreserved, reserved and %HH),
66
+ # the class the service kit's descriptor check uses (fabric_descriptor `_SOURCE_URI`, 0.49.3); a byte-order mark, a
67
+ # zero-width space, a non-ASCII path or an IDN host is refused (FAA-127).
68
+ _URI = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:(?:[A-Za-z0-9._~:/?#@!$&'()*+,;=-]|%[0-9A-Fa-f]{2})+\Z")
69
+ # Brackets belong to an IP-literal host only (RFC 3986 3.2.2); the literal is set aside before the character check.
70
+ _IP_LITERAL = re.compile(r"^([A-Za-z][A-Za-z0-9+.-]*://(?:[^/?#@\[\]]*@)?)\[[0-9A-Fa-f:.vV]+\]")
66
71
  # format: date-time (RFC 3339).
67
72
  _TIMESTAMP = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?([Zz]|[+-][0-9]{2}:[0-9]{2})\Z")
68
73
  FIELDS = {"protocol", "id", "providerId", "name", "summary", "manifest", "run", "source", "installedAt", "installedBy", "extensions"}
@@ -145,11 +150,14 @@ def validate_provider_entry(entry: Any) -> List[str]:
145
150
  source = entry["source"]
146
151
  if not isinstance(source, dict) or not set(source) <= {"repository"}:
147
152
  problems.append("source is {repository?}")
153
+ elif "repository" in source and isinstance(source["repository"], str) and (
154
+ _URI_PASSWORD.match(source["repository"]) or _secrets.repository_credential(source["repository"])):
155
+ # FAA-120, as the service kit: a repository is named, never reached with a credential in its URI. Said
156
+ # before the character check, so a password beside a character the URI class refuses (a backslash, as
157
+ # git reads one) is still named a credential (FAA-127).
158
+ problems.append("source.repository carries no password or credential: it names the repository")
148
159
  elif "repository" in source and not _is_uri(source["repository"]):
149
160
  problems.append("source.repository is an absolute URI")
150
- elif "repository" in source and (_URI_PASSWORD.match(source["repository"]) or _secrets.repository_credential(source["repository"])):
151
- # FAA-120, as the service kit: a repository is named, never reached with a credential in its URI.
152
- problems.append("source.repository carries no password or credential: it names the repository")
153
161
  if not isinstance(entry["installedAt"], str) or not _TIMESTAMP.fullmatch(entry["installedAt"]):
154
162
  problems.append("installedAt is an RFC 3339 date-time")
155
163
  if not isinstance(entry["installedBy"], str) or not 1 <= len(entry["installedBy"]) <= 200:
@@ -161,7 +169,7 @@ def validate_provider_entry(entry: Any) -> List[str]:
161
169
 
162
170
 
163
171
  def _is_uri(value: Any) -> bool:
164
- return isinstance(value, str) and bool(_URI.fullmatch(value))
172
+ return isinstance(value, str) and bool(_URI.fullmatch(_IP_LITERAL.sub(r"\1h", value, count=1)))
165
173
 
166
174
 
167
175
  def _run_problems(run: Any) -> List[str]:
@@ -255,7 +263,7 @@ def write_provider_entry(entry: Dict[str, Any], directory: Optional[Path] = None
255
263
  # re-pins (CT-192), so the writer does not emit it yet.
256
264
  raise ProviderError("The manifest's name follows a backslash, which a reader that has not re-pinned to DEC-0145 refuses (CT-192). "
257
265
  "Write the path with / before the name: ~/.../fabric-agent.json, which every Fabric release reads, or "
258
- "C:/.../fabric-agent.json, which Fabric reads only after v0.3.15 (its repin, CO-479).")
266
+ "C:/.../fabric-agent.json, which Fabric reads only from v0.3.16 (its repin, CO-479).")
259
267
  problems = manifest_problems(entry)
260
268
  if problems:
261
269
  raise ProviderError("Provider entry is invalid: %s." % "; ".join(problems))
@@ -285,6 +293,13 @@ def _now() -> str:
285
293
  return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
286
294
 
287
295
 
296
+
297
+ def entry_stem(path: str) -> str:
298
+ """CT-193 (FAC-SEM-014): the stem registry-rules.ts reads, `file.replace(/^.*[\\/]/, "").replace(/\\.json$/, "")`,
299
+ with ECMAScript's `.` (no line break: \\n, \\r, U+2028, U+2029) and `$` (the string's end) (FAA-189)."""
300
+ return re.sub(r"\.json\Z", "", re.sub(r"\A[^\n\r\u2028\u2029]*[\\/]", "", path, count=1))
301
+
302
+
288
303
  def main(argv: Optional[List[str]] = None) -> int:
289
304
  parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
290
305
  sub = parser.add_subparsers(dest="command", required=True)
@@ -320,7 +335,7 @@ def main(argv: Optional[List[str]] = None) -> int:
320
335
  entry = json.loads(args.path.read_text(encoding="utf-8"))
321
336
  problems = validate_provider_entry(entry)
322
337
  # CT-193 (FAC-SEM-014): the stem is what follows the last / or \ and loses .json, on every platform.
323
- stem = re.sub(r"\.json\Z", "", re.sub(r"^.*[\\/]", "", str(args.path), flags=re.S))
338
+ stem = entry_stem(str(args.path))
324
339
  if isinstance(entry, dict) and entry.get("id") != stem:
325
340
  problems.append("the file name %s is not <id>.json for %s (FAC-SEM-014)" % (args.path.name, entry.get("id")))
326
341
  if not problems:
@@ -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 _url_parts(value: str) -> Tuple[str, str, str]:
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 = _url_parts(value)
184
+ password, query, fragment = url_parts(value)
181
185
  if password:
182
186
  return True
183
187
  for part in (query, fragment):
@@ -45,6 +45,7 @@ is refused: it would travel with the source. The summary says whether Git chose
45
45
  Under a locale whose file-system encoding is not UTF-8 the check restarts itself in Python's UTF-8 mode, so a name,
46
46
  an argument and a file are read in one encoding. Standard library only.
47
47
  """
48
+ # Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
48
49
 
49
50
  # #region source-check — docs: plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md#the-check
50
51
 
@@ -200,7 +201,7 @@ def _url_secret(url: str, depth: int = 0) -> bool:
200
201
  url = url.rstrip(".,;)]}")
201
202
  if not _secrets._SCHEME.match(url):
202
203
  return False
203
- password, query, fragment = _secrets._url_parts(url)
204
+ password, query, fragment = _secrets.url_parts(url)
204
205
  # what says it is synthetic is the value, never the host or the path (`examples.blob.core.windows.net`)
205
206
  if len(password) >= 8 and re.search(r"[0-9]", password) and re.search(r"[A-Za-z]", password) and _real(unquote(password)):
206
207
  return True
@@ -9,7 +9,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
9
9
  compatibility: Python 3.9+ or Node.js 20+ for the kits (Node.js 22+ for a Linux service's instance lock); the probe needs Python 3.9+. launchd steps are macOS-only; Linux services install a systemd user unit and Windows ones a per-user Scheduled Task with the kits' writers. Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
10
10
  metadata:
11
11
  author: PassionCode.ai
12
- version: "0.78.0"
12
+ version: "0.80.2"
13
13
  contract-version: "0.1.0"
14
14
  extension: "fabric-service/0.1"
15
15
  extension-commit: "18f8d48762edc5bfb1433657cbfe1c16196df1a6"
@@ -168,7 +168,9 @@ Calling out: [the connectors reference](references/connectors.md) (`fabric_http`
168
168
  deadline, safe retries and a size cap; `fabric_openrouter` with the agent's own key, a
169
169
  spend ceiling before every attempt, receipts and a fallback). The draft-and-check step of a
170
170
  support agent on the kit: [`recipes/support-agent/`](recipes/support-agent/README.md); the whole agent:
171
- creating-fabric-agents' `references/agents-that-answer-people.md`.
171
+ creating-fabric-agents' `references/agents-that-answer-people.md`. A service that is only a dashboard:
172
+ [`recipes/dashboard-only/`](recipes/dashboard-only/README.md); a door that hands a key on sealed:
173
+ [`recipes/key-delivery-door/`](recipes/key-delivery-door/README.md).
172
174
 
173
175
  Startup order is the part that goes wrong; keep it exactly:
174
176
 
@@ -1,4 +1,4 @@
1
- /* fabric-dashboard-kit@1.1.5 · Fabric's button (Fabric Agent Contract DEC-0040, FB-1…FB-8, FL-1…FL-6)
1
+ /* fabric-dashboard-kit@1.1.6 · Fabric's button (Fabric Agent Contract DEC-0040, FB-1…FB-8, FL-1…FL-6)
2
2
  Load it same-origin, after fabric-dashboard.css, with the service it opens Fabric on:
3
3
  <script src="fabric-button.js" defer data-service="example-agent.default" data-service-name="Example Agent"
4
4
  data-project="project-a" data-project-name="Project A"></script>
@@ -11,7 +11,7 @@
11
11
  (function (root) {
12
12
  'use strict';
13
13
 
14
- var VERSION = '1.1.5';
14
+ var VERSION = '1.1.6';
15
15
  var COOKIE = 'fabric_button';
16
16
  var GET_FABRIC = 'https://passioncode.ai/fabric';
17
17
  var ABSENT_AFTER_MS = 1500;
@@ -69,7 +69,10 @@
69
69
  if (piece === '') return;
70
70
  var at = piece.indexOf('=');
71
71
  var name = firstKey(at < 0 ? piece : piece.slice(0, at));
72
- values.set(name, (values.get(name) || []).concat(at < 0 ? '' : decodeKeepingPlus(piece.slice(at + 1))));
72
+ var value = at < 0 ? '' : decodeKeepingPlus(piece.slice(at + 1));
73
+ var list = values.get(name);
74
+ if (list) list.push(value); // appended in place, as the contract's secret-shape.ts does (FAA-245)
75
+ else values.set(name, [value]);
73
76
  });
74
77
  return values;
75
78
  }
@@ -1,4 +1,4 @@
1
- /* fabric-dashboard-kit@1.1.5 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
1
+ /* fabric-dashboard-kit@1.1.6 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
2
2
  A Fabric service dashboard shown inside a host pane: usable from 360 CSS px up, navigation always
3
3
  reachable, wide content in its own scroll box, one vertical scroller, appearance from the host.
4
4
  Copy this file next to fabric-dashboard.js; do not edit it in place. Put the page's own rules in
@@ -1,4 +1,4 @@
1
- /* fabric-dashboard-kit@1.1.5 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
1
+ /* fabric-dashboard-kit@1.1.6 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
2
2
  Load it same-origin with <script src="fabric-dashboard.js" defer></script>: no inline script, no
3
3
  eval, nothing from a CDN. It applies the host's appearance (ED-6, ED-8), folds navigation that
4
4
  does not fit into a More menu (ED-3), makes wide boxes keyboard-reachable (ED-4) and keeps
@@ -8,7 +8,7 @@
8
8
  'use strict';
9
9
 
10
10
  var KIT = 'fabric-dashboard-kit';
11
- var VERSION = '1.1.5';
11
+ var VERSION = '1.1.6';
12
12
  /* fabric-dashboard-kit:theme-ids:begin */
13
13
  var THEMES = ['graphite', 'fabric', 'paper', 'forest', 'ocean', 'ember', 'contrast'];
14
14
  /* fabric-dashboard-kit:theme-ids:end */
@@ -148,6 +148,20 @@
148
148
  root.addEventListener('focus', applyAppearance);
149
149
  }
150
150
 
151
+ /* ---- resize observers write in the next frame (since 1.1.6) ----
152
+ A ResizeObserver callback that changes the size of what it watches, or of an ancestor, makes WebKit raise
153
+ "ResizeObserver loop completed with undelivered notifications". So a callback only schedules the work for the next
154
+ animation frame, cancelling one already scheduled, and the work itself skips a size it has already laid out. */
155
+ function nextFrame(work) {
156
+ var pending = null;
157
+ var raf = root.requestAnimationFrame;
158
+ return function () {
159
+ if (typeof raf !== 'function') { work(); return; }
160
+ if (pending !== null) root.cancelAnimationFrame(pending);
161
+ pending = raf.call(root, function () { pending = null; work(); });
162
+ };
163
+ }
164
+
151
165
  /* ---- navigation: fold what does not fit into More (ED-3) ---- */
152
166
  function foldNav(nav) {
153
167
  if (typeof nav.fdRefresh === 'function') { nav.fdRefresh(); return; } // folded already: refit, no second observers
@@ -212,8 +226,16 @@
212
226
  }
213
227
  }
214
228
  layout();
215
- if (typeof root.ResizeObserver === 'function') new root.ResizeObserver(function () { layout(); }).observe(nav);
216
- else root.addEventListener('resize', layout);
229
+ // The fit depends on the nav's width only: a callback for a height the fold itself changed lays nothing out.
230
+ var laidOutWidth = null;
231
+ var refit = nextFrame(function () {
232
+ var width = nav.clientWidth;
233
+ if (width === laidOutWidth) return;
234
+ laidOutWidth = width;
235
+ layout();
236
+ });
237
+ if (typeof root.ResizeObserver === 'function') new root.ResizeObserver(refit).observe(nav);
238
+ else root.addEventListener('resize', refit);
217
239
  // A page that moves aria-current (its route changed) gets a fresh summary and fit, without telling the kit.
218
240
  if (typeof root.MutationObserver === 'function') {
219
241
  new root.MutationObserver(layout).observe(nav, { subtree: true, attributes: true, attributeFilter: ['aria-current'] });
@@ -255,12 +277,16 @@
255
277
  var header = chrome.querySelector('.fd-header');
256
278
  var lift = 0;
257
279
  if (height > limit && header) lift = Math.min(Math.ceil(header.getBoundingClientRect().height), height);
258
- html.style.setProperty('--fd-chrome-top', lift ? -lift + 'px' : '0px');
259
- html.style.setProperty('--fd-chrome-h', Math.max(0, height - lift) + 'px');
280
+ var top = lift ? -lift + 'px' : '0px';
281
+ var pinned = Math.max(0, height - lift) + 'px';
282
+ // An unchanged value is not written again: a write restyles the page and can wake the observer once more.
283
+ if (html.style.getPropertyValue('--fd-chrome-top') !== top) html.style.setProperty('--fd-chrome-top', top);
284
+ if (html.style.getPropertyValue('--fd-chrome-h') !== pinned) html.style.setProperty('--fd-chrome-h', pinned);
260
285
  }
261
286
  set();
262
- if (typeof root.ResizeObserver === 'function') new root.ResizeObserver(set).observe(chrome);
263
- root.addEventListener('resize', set);
287
+ var reset = nextFrame(set);
288
+ if (typeof root.ResizeObserver === 'function') new root.ResizeObserver(reset).observe(chrome);
289
+ root.addEventListener('resize', reset);
264
290
  }
265
291
 
266
292
  function init() {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fabric-dashboard-kit",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "profile": "fabric-dashboard/0.1",
5
5
  "decision": "DEC-0036",
6
6
  "declarationKey": "https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1",
@@ -15,9 +15,9 @@
15
15
  "contrast"
16
16
  ],
17
17
  "files": {
18
- "fabric-dashboard.css": "cc7a96fa2c5eeb03d113e7a543ed88d43ce78c2808c411de199ea0fc6d55b957",
19
- "fabric-dashboard.js": "5df01193c590a1d8aa82a521954902fecf2d7f655df482165f90d7291b00087f",
20
- "fabric-button.js": "e945b3252f9a246826d06dbe7bd4be66e82d539a33728c3d3043fd61d4cbd681",
18
+ "fabric-dashboard.css": "7a90bbe1f1a2170df5dac0f596a3f302167bef9a7243457bb21dd0eb34f4d359",
19
+ "fabric-dashboard.js": "146ec9a0fd50af2df3245c02ec7dfba75b7d4e8cb5476fedb5ef727869fef1cc",
20
+ "fabric-button.js": "60c2b6d44571213e0237c29a94a4cf220f9de3efcfa697ac6a33777d1ad25fd6",
21
21
  "themes.json": "511839f1a1a83eb37fadfd132791474497d5c28c5091700b9aa31ad076e359b5"
22
22
  },
23
23
  "history": {
@@ -54,6 +54,11 @@
54
54
  "fabric-dashboard.css": "cc7a96fa2c5eeb03d113e7a543ed88d43ce78c2808c411de199ea0fc6d55b957",
55
55
  "fabric-dashboard.js": "5df01193c590a1d8aa82a521954902fecf2d7f655df482165f90d7291b00087f",
56
56
  "fabric-button.js": "e945b3252f9a246826d06dbe7bd4be66e82d539a33728c3d3043fd61d4cbd681"
57
+ },
58
+ "1.1.6": {
59
+ "fabric-dashboard.css": "7a90bbe1f1a2170df5dac0f596a3f302167bef9a7243457bb21dd0eb34f4d359",
60
+ "fabric-dashboard.js": "146ec9a0fd50af2df3245c02ec7dfba75b7d4e8cb5476fedb5ef727869fef1cc",
61
+ "fabric-button.js": "60c2b6d44571213e0237c29a94a4cf220f9de3efcfa697ac6a33777d1ad25fd6"
57
62
  }
58
63
  }
59
64
  }
@@ -0,0 +1,67 @@
1
+ # Recipe: a service that is only a dashboard
2
+
3
+ A `fabric-service/0.1` service with a dashboard and nothing else: no capabilities, no `mcp` surface, no `usage`
4
+ surface, no provider key and no token of anybody else. Fabric Dashboards asked for it (FD-171, board thread
5
+ `38c35e11`) for a page that shows what the agents of this computer are doing; this recipe is the shape agreed there
6
+ (FAA-268). Built only on the service kit (`fabric_service.py`, `fabric-service.mjs`). Tests:
7
+ `test/test_recipe_dashboard_only.py` and `test/recipe-dashboard-only.test.mjs`.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ python3 recipes/dashboard-only/install.py --id example-dashboard --folder ~/.local/share/example-dashboard
13
+ node recipes/dashboard-only/install.mjs --id example-dashboard --folder ~/.local/share/example-dashboard
14
+ ```
15
+
16
+ Both twins take the same flags and write the same descriptor bytes (but `installedAt`). Optional flags: `--instance`
17
+ (default `default`), `--name`, `--repository` (the repository its code is built from; by default the folder's own
18
+ `https://` origin, and a new install without one is refused by the kit, FAA-198), `--port` (this port and no other),
19
+ `--label` with `--plist` (a launchd job, below), `--services-dir` (default `FABRIC_SERVICES_DIR`, else the OS's),
20
+ `--json`.
21
+
22
+ | Output | Rule |
23
+ |---|---|
24
+ | last stdout line | `{"descriptor": "<path>", "key": "<id>.<instance>", "port": <n>}`; `--json` prints only it |
25
+ | a repeat with the same `--id` | changes nothing and answers the same line; a descriptor of that key another installer wrote, or one with another `--folder`, is refused and left as it is |
26
+ | exit codes | 0 installed or already installed, 1 refused (the reason on stderr, never a value), 2 usage |
27
+
28
+ The folder gets the token file (`service.token`, 0600, never printed) and later the event log, the login state and
29
+ `snapshot.json`. A refused install writes nothing.
30
+
31
+ ## The port
32
+
33
+ The installer takes the first port of 47300 to 47999 that no other descriptor of this account declares and that
34
+ `127.0.0.1` binds now, and writes the descriptor through the kit's `write_descriptor` / `writeDescriptor`, whose port
35
+ claim check refuses a port another descriptor declares; a port taken between the two is retried on the next one, up to
36
+ eight. That is the kit's port check as it stands, the older sense of a port claim
37
+ ([lifecycle.md](../../references/lifecycle.md), *Install, upgrade, uninstall*): a port another OS account listens on
38
+ or claims still passes (FAA-225). The contract's HA-1 claim file per OS account is not written, because its schema
39
+ waits on the contract's CT-228; the recipe writes it the day the kit does, with no change to its flags or its output.
40
+
41
+ ## Running it
42
+
43
+ ```bash
44
+ python3 recipes/dashboard-only/dashboard_service.py serve --descriptor <path the installer printed>
45
+ ```
46
+
47
+ It serves `/.well-known/fabric-service` (surfaces `dashboard` and `events` only), the events feed and the login code
48
+ for the service token, the one-time login and the signed-in page. Under launchd, pass `--label` and `--plist` on the
49
+ first install and install the job with the kit's `launchd_plist` and `launchd_install`
50
+ ([lifecycle.md](../../references/lifecycle.md)), whose `ProgramArguments` run the line above from a release
51
+ directory. `check_service.py <id>` passes every rule it can run on it (the test runs it).
52
+
53
+ ## Where its data comes from
54
+
55
+ The page shows `<folder>/snapshot.json`, which the person's agent writes from what it reads through the Fabric
56
+ Dashboards MCP (`list_services`, `spend`, `connections`) and Project Observatory's tools. The service itself calls
57
+ nobody, so it holds no token but its own:
58
+
59
+ ```json
60
+ {"takenAt": "2026-11-12T08:00:00Z",
61
+ "sections": [{"title": "Services", "source": "fabric-dashboards list_services",
62
+ "columns": ["Service", "State"], "rows": [["example-agent.default", "ready"]]}]}
63
+ ```
64
+
65
+ A snapshot that is missing, cannot be read, does not have this shape, is over 1 MB or is older than a day is
66
+ reported in `degraded` (source `snapshot`) and never shown as data. The well-known document declares the snapshot as
67
+ its one connection (`kind: store`), so a host draws where the page's numbers come from.