@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
|
@@ -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.
|
|
12
|
+
version: "0.81.0"
|
|
13
13
|
contract-version: "0.1.0"
|
|
14
14
|
extension: "fabric-service/0.1"
|
|
15
|
-
extension-commit: "
|
|
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)
|
|
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.
|