@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.
Files changed (120) hide show
  1. package/CHANGELOG.md +213 -0
  2. package/README.md +13 -2
  3. package/SECURITY.md +23 -8
  4. package/bin/fabric-agent-adapter.js +50 -5
  5. package/package.json +1 -1
  6. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.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 +12 -4
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +1 -1
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +44 -7
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +107 -2
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +26 -6
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +9 -1
  15. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +24 -5
  16. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +57 -76
  17. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_restart.py +56 -0
  18. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +912 -0
  19. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/new_agent.py +308 -0
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +8 -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/recipes/support-agent/README.md +1 -1
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +4 -4
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +4 -2
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +12 -4
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +7 -3
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +12 -1
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +54 -3
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +6 -3
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +33 -12
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -0
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -0
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -7
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +44 -3
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +21 -0
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +26 -6
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +24 -33
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +12 -6
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +136 -3
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +3 -0
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +1 -0
  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-descriptor.mjs +118 -3
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +266 -0
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +72 -7
  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-package.mjs +98 -1
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +7 -3
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +10 -3
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +11 -0
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +114 -20
  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_board_session.py +3 -2
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +2 -0
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +97 -3
  64. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +50 -8
  65. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +120 -3
  66. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +316 -0
  67. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +40 -3
  68. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +15 -2
  69. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +2 -2
  70. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +358 -0
  71. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +103 -1
  72. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +28 -13
  73. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
  74. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +24 -5
  75. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +23 -0
  76. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +110 -9
  77. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +12 -1
  78. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +469 -29
  79. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +4 -1
  80. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +18 -6
  81. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +44 -0
  82. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +4 -4
  83. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/communication-agent-template.md +19 -328
  84. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +12 -4
  85. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +1 -1
  86. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/new-agent.md +42 -0
  87. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +7 -1
  88. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +44 -7
  89. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +28 -0
  90. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/working-rules.md +54 -0
  91. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/access.schema.json +50 -0
  92. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/connections.schema.json +23 -0
  93. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/data-rooms.schema.json +74 -0
  94. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/manifest.schema.json +170 -0
  95. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/template-header.schema.json +25 -0
  96. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +26 -6
  97. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/template_check.py +491 -0
  98. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/CHANGELOG.md +7 -0
  99. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/TEMPLATE.md +347 -0
  100. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/access.json +24 -0
  101. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/assembly.md +15 -0
  102. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/checks/acceptance.md +17 -0
  103. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/connections.json +29 -0
  104. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/data-rooms.json +9 -0
  105. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/manifest.json +32 -0
  106. package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
  107. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +21 -21
  108. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +4 -4
  109. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/cleanup.md +30 -0
  110. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +2 -1
  111. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +3 -2
  112. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/remote-sessions.md +75 -0
  113. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +28 -13
  114. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/task-context.md +58 -0
  115. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/waits.md +36 -0
  116. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +14 -0
  117. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +143 -36
  118. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +136 -15
  119. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +78 -3
  120. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +9 -2
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env python3
2
+ """Create a Fabric agent project with the contract built in, in one command (FAA-270, North Star G21).
3
+
4
+ python3 new_agent.py <folder> --id <agent-id> [--profile mcp|a2a|local-runner] [--service]
5
+ [--capability NAME] [--effect CLASS] [--force] [--json]
6
+
7
+ It writes, into an empty (or new) folder:
8
+
9
+ - the provider bundle `adapt_project.py scaffold` writes, pinned to the contract (`fabric-agent.json`,
10
+ `fabric-contract.lock.json`, `fabric/`), every value it invents a sentinel `adapt_project.py check` names;
11
+ - `fabric.source.json`, the agent's source manifest (DEC-0149, `fabric-source/1`): what is source, default deny, and the
12
+ slots an installation fills (the publisher's URI behind `provider.createdBy`, the placement record and, for a
13
+ service, its supervisor label and port), never a value;
14
+ - the placement guard (`src/placement.py`, with its tests in `tests/`): at start the agent reads Project Observatory's
15
+ placement record (`observatory-placement/1`) and refuses to start where the record places it on another machine,
16
+ starts degraded when it cannot read it, and starts when there is none; `config/placement.example.json` shows the
17
+ record's shape on neutral values;
18
+ - `README.md`, `LICENSE` (all rights reserved until the owner names the terms), `.gitignore`;
19
+ - with `--service`: `service/`, the service kit's sample (`service.py`, a fabric-service/0.1 service with one capability
20
+ and a job) and the kit modules it imports, copied by `fabric_kit.py install`, whose digests `fabric_kit.py status`
21
+ checks later.
22
+
23
+ A repeat with the same arguments in a folder this command made changes nothing and answers the same line. A folder that
24
+ holds anything else is refused unless `--force`, which writes the planned files over it and leaves every other file as
25
+ it is. The last stdout line is `{"folder": "<path>", "id": "<id>", "profile": "<profile>", "service": <bool>,
26
+ "created": [<paths>], "repeated": <bool>}` (`--json` prints only it). Exit 0 made or already made, 1 refused (the
27
+ reason on stderr), 2 usage. Writes nothing outside the folder; reads no key and no instance data. Standard library only.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import argparse
33
+ import json
34
+ from pathlib import Path
35
+ import re
36
+ import shutil
37
+ import sys
38
+ from typing import Any, Dict, List, Optional, Sequence
39
+
40
+ HERE = Path(__file__).resolve().parent
41
+ sys.path.insert(0, str(HERE))
42
+ import adapt_project as ap # noqa: E402 - the scaffolder and the source manifest, beside this file
43
+
44
+ VERSION = "0.1.0"
45
+ RECORD = "fabric/new-agent.json"
46
+ #: The agent's id: the package's `name` grammar (`fabric-package.schema.json#/$defs/name`), kebab case.
47
+ AGENT_ID = re.compile(r"[a-z][a-z0-9]*(?:-[a-z0-9]+)*\Z")
48
+ CAPABILITY = re.compile(r"[a-z][a-z0-9._-]{1,127}\Z")
49
+ KIT = HERE.parent.parent / "building-fabric-services" / "scripts"
50
+ #: What the sample service imports; `fabric_kit.py install` copies each one's closure.
51
+ SERVICE_MODULES = ("fabric_service", "fabric_interop", "fabric_board", "fabric_hub")
52
+
53
+
54
+ class Refused(Exception):
55
+ """The folder cannot be made as asked; the reason is printed, never a value."""
56
+
57
+
58
+ PLACEMENT_PY = '''"""The placement guard: at start, before the agent polls, schedules or listens, it reads Project Observatory's
59
+ placement record and runs only on the machine that record names (creating-fabric-agents, where-agents-run.md).
60
+
61
+ verdict(service_id, label) -> {"state": "ok" | "placed-elsewhere" | "placement-unreadable", "machine"?}
62
+
63
+ `placed-elsewhere`: the record's `elsewhere` list names this agent (its `serviceId` or its supervisor `label`) on a
64
+ machine that is not this one: refuse to start, log one line, report it as degraded. `placement-unreadable`: the file is
65
+ there and cannot be read, or is of another schema: start, and report it as degraded. No file: start.
66
+ """
67
+ import json
68
+ import os
69
+ from pathlib import Path
70
+ from typing import Any, Dict, Optional
71
+
72
+ SCHEMA = "observatory-placement/1"
73
+
74
+
75
+ def placement_path(env: Optional[Dict[str, str]] = None) -> Path:
76
+ env = os.environ if env is None else env
77
+ base = env.get("XDG_DATA_HOME") or ""
78
+ root = Path(base) if os.path.isabs(base) else Path.home() / ".local" / "share"
79
+ return root / "project-observatory" / "placement.json"
80
+
81
+
82
+ def verdict(service_id: str, label: str, path: Optional[Path] = None) -> Dict[str, Any]:
83
+ path = placement_path() if path is None else path
84
+ try:
85
+ data = path.read_bytes()
86
+ except FileNotFoundError:
87
+ return {"state": "ok"}
88
+ except OSError:
89
+ return {"state": "placement-unreadable"}
90
+ try:
91
+ doc = json.loads(data.decode("utf-8"))
92
+ except ValueError:
93
+ return {"state": "placement-unreadable"}
94
+ if not isinstance(doc, dict) or doc.get("schema") != SCHEMA or not isinstance(doc.get("elsewhere"), list):
95
+ return {"state": "placement-unreadable"}
96
+ for entry in doc["elsewhere"]:
97
+ if not isinstance(entry, dict):
98
+ continue
99
+ ours = (service_id and entry.get("serviceId") == service_id) or (label and entry.get("label") == label)
100
+ if ours and entry.get("placedOn") and entry.get("placedOn") != doc.get("machine"):
101
+ return {"state": "placed-elsewhere", "machine": entry.get("placedOn")}
102
+ return {"state": "ok"}
103
+ '''
104
+
105
+ PLACEMENT_TEST = '''"""The placement guard: one copy placed elsewhere refuses, one with no file and one with a malformed file start."""
106
+ import json
107
+ from pathlib import Path
108
+ import sys
109
+ import tempfile
110
+ import unittest
111
+
112
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
113
+ import placement # noqa: E402
114
+
115
+ EXAMPLE = Path(__file__).resolve().parents[1] / "config" / "placement.example.json"
116
+
117
+
118
+ class PlacementTests(unittest.TestCase):
119
+ def setUp(self):
120
+ self.folder = tempfile.TemporaryDirectory()
121
+ self.addCleanup(self.folder.cleanup)
122
+ self.path = Path(self.folder.name) / "placement.json"
123
+
124
+ def test_placed_elsewhere_refuses(self):
125
+ self.path.write_text(EXAMPLE.read_text(encoding="utf-8"), encoding="utf-8")
126
+ found = placement.verdict("{service_id}", "{label}", self.path)
127
+ self.assertEqual(found["state"], "placed-elsewhere")
128
+
129
+ def test_no_file_starts(self):
130
+ self.assertEqual(placement.verdict("{service_id}", "{label}", self.path), {{"state": "ok"}})
131
+
132
+ def test_a_malformed_file_starts_degraded(self):
133
+ self.path.write_text("{{not json", encoding="utf-8")
134
+ self.assertEqual(placement.verdict("{service_id}", "{label}", self.path), {{"state": "placement-unreadable"}})
135
+ self.path.write_text(json.dumps({{"schema": "another/1"}}), encoding="utf-8")
136
+ self.assertEqual(placement.verdict("{service_id}", "{label}", self.path)["state"], "placement-unreadable")
137
+
138
+ def test_placed_on_this_machine_starts(self):
139
+ doc = json.loads(EXAMPLE.read_text(encoding="utf-8"))
140
+ doc["machine"] = doc["elsewhere"][0]["placedOn"]
141
+ self.path.write_text(json.dumps(doc), encoding="utf-8")
142
+ self.assertEqual(placement.verdict("{service_id}", "{label}", self.path), {{"state": "ok"}})
143
+
144
+
145
+ if __name__ == "__main__":
146
+ unittest.main()
147
+ '''
148
+
149
+ README = '''# {agent_id}
150
+
151
+ A Fabric agent made with the Fabric Agent Adapter's `new` command, contract `{contract}` at `{commit}`.
152
+
153
+ ## Layout
154
+
155
+ - `fabric-agent.json`, `fabric-contract.lock.json`, `fabric/`: the provider bundle, pinned to the contract. Every value
156
+ the scaffold invented is a sentinel: `adapt_project.py check` names each one until it is replaced.
157
+ - `fabric.source.json`: the agent's source manifest (DEC-0149): what is source, and the slots an installation fills,
158
+ never a value. `fabric_source_check.py check .` reads the source under it (exit 0 before every release).
159
+ - `src/placement.py`: the placement guard the agent runs at start, before it polls, schedules or listens.
160
+ {service_line}
161
+ ## Configure
162
+
163
+ Keys are read by name from the environment or the secret store, never from a file of this repository. Slots:
164
+ {slots}
165
+
166
+ ## Data
167
+
168
+ The agent's data lives apart from this source, in its data home (`<agent>-data`, or the service kit's folders), from
169
+ the first commit.
170
+ '''
171
+
172
+ GITIGNORE = """# the instance: real configuration, keys and what running the agent leaves
173
+ config/*.json
174
+ !config/*.example.json
175
+ .env
176
+ .env.*
177
+ !.env.example
178
+ *.token
179
+ *.credential
180
+ __pycache__/
181
+ *.pyc
182
+ """
183
+
184
+
185
+ def _placement_example(service_id: str, label: str) -> str:
186
+ return ap._json_text({"schema": "observatory-placement/1", "machine": "example-machine-a", "updatedAt": "2026-01-01T00:00:00Z",
187
+ "elsewhere": [{"label": label, "serviceId": service_id, "placedOn": "example-machine-b",
188
+ "since": "2026-01-01T00:00:00Z", "by": "full machine-move leave"}]})
189
+
190
+
191
+ def plan(args: argparse.Namespace) -> Dict[str, str]:
192
+ """Every file the command writes, by its path, with its text (the scaffold's own included)."""
193
+ agent_id = args.id
194
+ capability = args.capability or "%s.run" % agent_id.replace("-", "_")
195
+ if not CAPABILITY.match(capability):
196
+ raise Refused("--capability %r is not a capability name (lower case, `domain.action`)" % capability)
197
+ scaffold = argparse.Namespace(
198
+ profile=args.profile, provider_id="urn:fabric:provider:%s" % agent_id, provider_name=agent_id,
199
+ capability_id="urn:fabric:capability:%s" % capability, capability_name=capability,
200
+ schema_base="https://replace.invalid/%s/schemas" % agent_id, mcp_mode="streamable-http",
201
+ mcp_url="https://replace.invalid/mcp", agent_card_url="https://replace.invalid/.well-known/agent-card.json",
202
+ runner_kind="replace-me", executable_ref="urn:executable:replace-me", runtime_arg=[], effect=args.effect,
203
+ idempotency="none", cost_preset=[], default_cost_preset=None, job=False, created_by=None, identity_method=None,
204
+ data_class=[], probe_assertion=[])
205
+ files = dict(ap._generated_files(scaffold))
206
+ service_id = "%s.default" % agent_id
207
+ label = "com.example.%s" % agent_id
208
+ include = ["README.md", "LICENSE", ".gitignore", "src/", "tests/", "config/placement.example.json",
209
+ "fabric-agent.json", "fabric-contract.lock.json", "fabric/"] + (["service/"] if args.service else [])
210
+ manifest = ap.source_manifest(include, "%s, a Fabric agent" % agent_id, service=args.service)
211
+ slots = "\n".join("- `%s` (%s%s): %s" % (s["name"], s["kind"], ", optional" if s.get("optional") else "", s["purpose"])
212
+ for s in manifest["slots"])
213
+ files.update({
214
+ ap.SOURCE_FILE: ap._json_text(manifest),
215
+ "README.md": README.format(agent_id=agent_id, contract=ap.CONTRACT_VERSION, commit=ap.CONTRACT_COMMIT[:12], slots=slots,
216
+ service_line="- `service/`: the service kit's sample, run as `python3 service/service.py "
217
+ "serve --id %s --port <port> --data-dir <data>`, and the kit modules it imports.\n" % agent_id
218
+ if args.service else ""),
219
+ "LICENSE": "Copyright (c) the owner of %s. All rights reserved, until the owner names other terms.\n" % agent_id,
220
+ ".gitignore": GITIGNORE,
221
+ "src/placement.py": PLACEMENT_PY,
222
+ "tests/test_placement.py": PLACEMENT_TEST.format(service_id=service_id, label=label),
223
+ "config/placement.example.json": _placement_example(service_id, label),
224
+ })
225
+ record = {"tool": "fabric-agent-adapter new %s" % VERSION, "id": agent_id, "profile": args.profile, "service": args.service,
226
+ "capability": capability, "effect": args.effect, "contract": ap.CONTRACT_COMMIT}
227
+ files[RECORD] = ap._json_text(record)
228
+ return files
229
+
230
+
231
+ def _service_files(folder: Path) -> List[str]:
232
+ """Copy the kit's sample and the closure of what it imports into `service/` (fabric_kit.py install)."""
233
+ import importlib.util
234
+ found = importlib.util.spec_from_file_location("fabric_kit", KIT / "fabric_kit.py") # the kit's own installer
235
+ fabric_kit = importlib.util.module_from_spec(found)
236
+ found.loader.exec_module(fabric_kit) # type: ignore[union-attr]
237
+ target = folder / "service"
238
+ result = fabric_kit.install(target, "python", list(SERVICE_MODULES))
239
+ if result["refused"]:
240
+ raise Refused("service/ holds kit modules this kit does not recognise: %s" % ", ".join(result["refused"]))
241
+ shutil.copyfile(KIT / "sample_service.py", target / "service.py")
242
+ return ["service/" + name for name in result["copied"] + result["kept"]] + ["service/service.py"]
243
+
244
+
245
+ def make(folder: Path, args: argparse.Namespace) -> Dict[str, Any]:
246
+ if not AGENT_ID.match(args.id) or len(args.id) > 64:
247
+ raise Refused("--id %r is not an agent id (lower case words joined by -, at most 64 characters)" % args.id)
248
+ files = plan(args)
249
+ answer = {"folder": str(folder), "id": args.id, "profile": args.profile, "service": args.service}
250
+ if folder.exists() and not folder.is_dir():
251
+ raise Refused("%s is not a folder" % folder.name)
252
+ present = sorted(p.relative_to(folder).as_posix() for p in folder.rglob("*")) if folder.is_dir() else []
253
+ if present:
254
+ record = folder / RECORD
255
+ try:
256
+ same = record.is_file() and json.loads(record.read_text(encoding="utf-8")) == json.loads(files[RECORD])
257
+ except ValueError:
258
+ same = False
259
+ if same and all((folder / path).is_file() for path in files) and not args.force:
260
+ return dict(answer, created=[], repeated=True)
261
+ if not args.force:
262
+ raise Refused("the folder is not empty and was not made by this command with these arguments: "
263
+ "name another folder, or pass --force to write the planned files over it")
264
+ for path, text in files.items():
265
+ target = folder / path
266
+ target.parent.mkdir(parents=True, exist_ok=True)
267
+ target.write_text(text, encoding="utf-8")
268
+ created = sorted(files)
269
+ if args.service:
270
+ created = sorted(created + _service_files(folder))
271
+ return dict(answer, created=created, repeated=False)
272
+
273
+
274
+ def build_parser() -> argparse.ArgumentParser:
275
+ parser = argparse.ArgumentParser(prog="new_agent.py", description="Create a Fabric agent project with the contract built in.")
276
+ parser.add_argument("folder", type=Path)
277
+ parser.add_argument("--id", required=True, help="the agent's id, kebab case (example-agent)")
278
+ parser.add_argument("--profile", choices=("mcp", "a2a", "local-runner"), default="mcp")
279
+ parser.add_argument("--service", action="store_true", help="also wire the service kit's sample (fabric-service/0.1)")
280
+ parser.add_argument("--capability", help="its capability, `domain.action` (default <id>.run)")
281
+ parser.add_argument("--effect", choices=ap.EFFECT_CLASSES, default="none",
282
+ help="the capability's effect class (default none: declare money, deletion or publication now)")
283
+ parser.add_argument("--force", action="store_true", help="write the planned files into a folder that holds others")
284
+ parser.add_argument("--json", action="store_true", help="print only the last line")
285
+ return parser
286
+
287
+
288
+ def main(argv: Optional[Sequence[str]] = None) -> int:
289
+ try:
290
+ args = build_parser().parse_args(argv)
291
+ except SystemExit as exc:
292
+ return exc.code if isinstance(exc.code, int) else 2
293
+ try:
294
+ answer = make(args.folder.expanduser().resolve(), args)
295
+ except (Refused, ap.AdaptationError, OSError) as exc:
296
+ print("new_agent.py: %s" % (exc if not isinstance(exc, OSError) else "the folder cannot be written (%s)"
297
+ % type(exc).__name__), file=sys.stderr)
298
+ return 1
299
+ if not args.json:
300
+ print("%s %s: %d file(s)%s" % ("already made" if answer["repeated"] else "made", args.id, len(answer["created"]),
301
+ "" if answer["repeated"] else "; next: replace the sentinels `adapt_project.py check` "
302
+ "names, then run fabric_source_check.py check on the folder"))
303
+ print(json.dumps(answer, ensure_ascii=False))
304
+ return 0
305
+
306
+
307
+ if __name__ == "__main__":
308
+ raise SystemExit(main())
@@ -9,10 +9,10 @@ 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.79.1"
12
+ version: "0.81.0"
13
13
  contract-version: "0.1.0"
14
14
  extension: "fabric-service/0.1"
15
- extension-commit: "18f8d48762edc5bfb1433657cbfe1c16196df1a6"
15
+ extension-commit: "93e7db164c7829e2783b4b7705c4113bb5201131"
16
16
  ---
17
17
 
18
18
  # Building Fabric services
@@ -82,7 +82,9 @@ MCP registration into a client config, the login flow or the CSRF header.
82
82
  When other agents or Fabric call the service over MCP, it follows `fabric-interop/0.1`:
83
83
  each manifest capability is the tool of its name, long work is a job with a stable id,
84
84
  a question for a person is an elicitation, and every call carries one trace. A job a limit stops or pauses, and an
85
- effect nobody saw, have their own codes and a key per caller (DEC-0087). Read
85
+ effect nobody saw, have their own codes and a key per caller (DEC-0087); work held by the service's own limits is
86
+ said on the job, pushed to the operator once per hold and lifted by their scoped answer (`scripts/fabric_holds.py`,
87
+ FAA-298). Read
86
88
  [the interop reference](references/interop.md) before writing an MCP tool; the helpers
87
89
  are in [`scripts/fabric_interop.py`](scripts/fabric_interop.py) and
88
90
  [`scripts/fabric-interop.mjs`](scripts/fabric-interop.mjs). When the callers are agents of the estate, the
@@ -168,7 +170,9 @@ Calling out: [the connectors reference](references/connectors.md) (`fabric_http`
168
170
  deadline, safe retries and a size cap; `fabric_openrouter` with the agent's own key, a
169
171
  spend ceiling before every attempt, receipts and a fallback). The draft-and-check step of a
170
172
  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`.
173
+ creating-fabric-agents' `references/agents-that-answer-people.md`. A service that is only a dashboard:
174
+ [`recipes/dashboard-only/`](recipes/dashboard-only/README.md); a door that hands a key on sealed:
175
+ [`recipes/key-delivery-door/`](recipes/key-delivery-door/README.md).
172
176
 
173
177
  Startup order is the part that goes wrong; keep it exactly:
174
178
 
@@ -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.