@passioncode-ai/fabric-agent-adapter 0.3.1 → 0.4.3
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 +82 -0
- package/LICENSE +151 -17
- package/README.md +51 -6
- package/SECURITY.md +16 -3
- package/bin/fabric-agent-adapter.js +87 -43
- package/package.json +13 -8
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +5 -6
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +8 -5
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +240 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +32 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/events-and-notifications.md +44 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +63 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +32 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +83 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +58 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +373 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +380 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +663 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +226 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +23 -5
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""A complete, minimal fabric-service/0.1 service built on fabric_service.py.
|
|
3
|
+
|
|
4
|
+
It is the worked example the skill points at, the target check_service.py tests
|
|
5
|
+
itself against, and the fixture Fabric Dashboards runs its end-to-end tests on.
|
|
6
|
+
|
|
7
|
+
sample_service.py serve --port 47190 --data-dir DIR [--id sample] [--instance default]
|
|
8
|
+
sample_service.py register --port 47190 --data-dir DIR [--services-dir DIR]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import html
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
from http.server import BaseHTTPRequestHandler
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
import signal
|
|
20
|
+
import sys
|
|
21
|
+
import threading
|
|
22
|
+
from typing import Any, Dict, Optional
|
|
23
|
+
from urllib.parse import parse_qs, urlparse
|
|
24
|
+
|
|
25
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
26
|
+
import fabric_service as fs # noqa: E402
|
|
27
|
+
|
|
28
|
+
VERSION = "0.1.0"
|
|
29
|
+
PAGE = """<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
30
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"><title>{name}</title>
|
|
31
|
+
<style>body{{font:15px system-ui;margin:2rem;color:#e9e4ec;background:#0a070d}}li{{margin:.3rem 0}}</style>
|
|
32
|
+
</head><body><h1>{name}</h1><p>Status: {status}</p><ul>{rows}</ul></body></html>"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Service:
|
|
36
|
+
def __init__(self, args: argparse.Namespace):
|
|
37
|
+
self.id = args.id
|
|
38
|
+
self.instance = args.instance
|
|
39
|
+
self.name = args.name
|
|
40
|
+
self.port = args.port
|
|
41
|
+
self.data = Path(args.data_dir)
|
|
42
|
+
self.token_file = Path(args.token_file) if args.token_file else self.data / "service.token"
|
|
43
|
+
self.degraded = [{"source": "demo", "reason": args.degraded}] if args.degraded else []
|
|
44
|
+
self.started_at = fs.now_iso()
|
|
45
|
+
self.build = {"commit": os.environ.get("FABRIC_BUILD_COMMIT", "0000000"), "builtAt": self.started_at}
|
|
46
|
+
|
|
47
|
+
def start(self) -> None:
|
|
48
|
+
# Rule: the lock comes before ANY side effect, including creating the token.
|
|
49
|
+
self.lock = fs.hold_single_instance(self.data)
|
|
50
|
+
self.token = fs.ensure_token(self.token_file)
|
|
51
|
+
self.log = fs.JsonlEventLog(self.data / "events.jsonl")
|
|
52
|
+
self.codes = fs.LoginCodes(self.data / "auth")
|
|
53
|
+
self.log.append("service.started", "info", "%s started on build %s." % (self.name, self.build["commit"]))
|
|
54
|
+
|
|
55
|
+
def well_known(self) -> Dict[str, Any]:
|
|
56
|
+
return fs.build_well_known(
|
|
57
|
+
service_id=self.id, instance=self.instance, name=self.name, version=VERSION, build=self.build,
|
|
58
|
+
started_at=self.started_at, status="ready", degraded=self.degraded,
|
|
59
|
+
summary=[{"label": "Events", "value": len(self.log.fetch(None, fs.EVENTS_MAX_LIMIT))}],
|
|
60
|
+
surfaces={"dashboard": {"path": "/", "login": True}, "events": {"path": "/fabric/v1/events"}},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def make_handler(svc: Service):
|
|
65
|
+
class Handler(BaseHTTPRequestHandler):
|
|
66
|
+
server_version = "fabric-sample/" + VERSION
|
|
67
|
+
|
|
68
|
+
def log_message(self, fmt: str, *args: Any) -> None: # quiet by default
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
def _send(self, status: int, body: Any = None, headers: Optional[Dict[str, str]] = None,
|
|
72
|
+
content_type: str = "application/json") -> None:
|
|
73
|
+
payload = b""
|
|
74
|
+
if body is not None:
|
|
75
|
+
payload = body.encode() if isinstance(body, str) else json.dumps(body).encode()
|
|
76
|
+
self.send_response(status)
|
|
77
|
+
self.send_header("Cache-Control", "no-store")
|
|
78
|
+
self.send_header("X-Content-Type-Options", "nosniff")
|
|
79
|
+
self.send_header("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'self'")
|
|
80
|
+
if body is not None:
|
|
81
|
+
self.send_header("Content-Type", content_type)
|
|
82
|
+
self.send_header("Content-Length", str(len(payload)))
|
|
83
|
+
for key, value in (headers or {}).items():
|
|
84
|
+
self.send_header(key, value)
|
|
85
|
+
self.end_headers()
|
|
86
|
+
if payload:
|
|
87
|
+
self.wfile.write(payload)
|
|
88
|
+
|
|
89
|
+
def _guard(self) -> bool:
|
|
90
|
+
reason = fs.check_request(svc.port, self.headers.get("Host"), self.headers.get("Origin"),
|
|
91
|
+
self.headers.get("Sec-Fetch-Site"))
|
|
92
|
+
if reason:
|
|
93
|
+
self._send(403, {"error": reason})
|
|
94
|
+
return False
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
def _token_ok(self) -> bool:
|
|
98
|
+
if fs.token_matches(self.headers.get("Authorization"), svc.token):
|
|
99
|
+
return True
|
|
100
|
+
self._send(401, {"error": "The service token is required."}, {"WWW-Authenticate": "Bearer"})
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
def _session_ok(self) -> bool:
|
|
104
|
+
return svc.codes.session_valid(fs.cookie_value(self.headers.get("Cookie")))
|
|
105
|
+
|
|
106
|
+
def do_GET(self) -> None: # noqa: N802
|
|
107
|
+
if not self._guard():
|
|
108
|
+
return
|
|
109
|
+
url = urlparse(self.path)
|
|
110
|
+
query = parse_qs(url.query)
|
|
111
|
+
if url.path == "/.well-known/fabric-service":
|
|
112
|
+
return self._send(200, svc.well_known())
|
|
113
|
+
if url.path == "/fabric/v1/events":
|
|
114
|
+
if not self._token_ok():
|
|
115
|
+
return
|
|
116
|
+
try:
|
|
117
|
+
limit = fs.parse_limit((query.get("limit") or [None])[0])
|
|
118
|
+
page = fs.events_page(svc.log.fetch, (query.get("after") or [None])[0], limit)
|
|
119
|
+
except fs.ServiceError as exc:
|
|
120
|
+
return self._send(400, {"error": str(exc)})
|
|
121
|
+
return self._send(200, page)
|
|
122
|
+
if url.path == "/fabric/v1/login":
|
|
123
|
+
cookie = svc.codes.redeem((query.get("code") or [None])[0])
|
|
124
|
+
if not cookie:
|
|
125
|
+
return self._send(403, "This sign-in link was already used or has expired.", content_type="text/plain")
|
|
126
|
+
return self._send(302, None, {"Location": "/", "Set-Cookie": fs.session_cookie_header(cookie)})
|
|
127
|
+
if url.path == "/":
|
|
128
|
+
if not self._session_ok():
|
|
129
|
+
return self._send(401, "Open this dashboard from Fabric Dashboards.", content_type="text/plain")
|
|
130
|
+
rows = "".join("<li>%s — %s</li>" % (html.escape(e["at"]), html.escape(e["text"]))
|
|
131
|
+
for e in reversed(svc.log.fetch(None, 20)))
|
|
132
|
+
return self._send(200, PAGE.format(name=html.escape(svc.name), status="ready", rows=rows),
|
|
133
|
+
content_type="text/html; charset=utf-8")
|
|
134
|
+
self._send(404, {"error": "Not found."})
|
|
135
|
+
|
|
136
|
+
def do_POST(self) -> None: # noqa: N802
|
|
137
|
+
if not self._guard():
|
|
138
|
+
return
|
|
139
|
+
url = urlparse(self.path)
|
|
140
|
+
if url.path == "/fabric/v1/login-code":
|
|
141
|
+
if not self._token_ok():
|
|
142
|
+
return
|
|
143
|
+
return self._send(200, svc.codes.issue())
|
|
144
|
+
if url.path == "/api/emit":
|
|
145
|
+
if not (self._session_ok() or fs.token_matches(self.headers.get("Authorization"), svc.token)):
|
|
146
|
+
return self._send(401, {"error": "Sign in first."})
|
|
147
|
+
if self.headers.get("X-Fabric-Request") != "1":
|
|
148
|
+
return self._send(403, {"error": "Missing request header."})
|
|
149
|
+
length = min(int(self.headers.get("Content-Length") or 0), 65536)
|
|
150
|
+
body = json.loads(self.rfile.read(length) or b"{}")
|
|
151
|
+
event = svc.log.append(body.get("kind", "demo.note"), body.get("level", "info"),
|
|
152
|
+
body.get("text", "A note from the sample service."),
|
|
153
|
+
notify=bool(body.get("notify")), link=body.get("link"))
|
|
154
|
+
return self._send(200, event)
|
|
155
|
+
self._send(404, {"error": "Not found."})
|
|
156
|
+
|
|
157
|
+
return Handler
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def make_server(svc: Service) -> fs.LoopbackHTTPServer:
|
|
161
|
+
return fs.LoopbackHTTPServer(("127.0.0.1", svc.port), make_handler(svc))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def serve(args: argparse.Namespace) -> int:
|
|
165
|
+
svc = Service(args)
|
|
166
|
+
svc.start()
|
|
167
|
+
server = make_server(svc)
|
|
168
|
+
|
|
169
|
+
def stop(signum: int, _frame: Any) -> None:
|
|
170
|
+
svc.log.append("service.stopping", "info", "%s is stopping." % svc.name)
|
|
171
|
+
threading.Thread(target=server.shutdown, daemon=True).start()
|
|
172
|
+
|
|
173
|
+
signal.signal(signal.SIGTERM, stop)
|
|
174
|
+
signal.signal(signal.SIGINT, stop)
|
|
175
|
+
try:
|
|
176
|
+
server.serve_forever(poll_interval=0.2)
|
|
177
|
+
finally:
|
|
178
|
+
server.server_close()
|
|
179
|
+
svc.lock.release()
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def register(args: argparse.Namespace) -> int:
|
|
184
|
+
data = Path(args.data_dir)
|
|
185
|
+
descriptor = {
|
|
186
|
+
"protocol": fs.PROTOCOL, "id": args.id, "instance": args.instance, "name": args.name,
|
|
187
|
+
"summary": "Sample fabric-service/0.1 service.",
|
|
188
|
+
"origin": "http://127.0.0.1:%d" % args.port,
|
|
189
|
+
"auth": {"tokenFile": str(Path(args.token_file) if args.token_file else data / "service.token")},
|
|
190
|
+
"lifecycle": {"manager": "launchd", "label": args.label, "plist": args.plist} if args.label
|
|
191
|
+
else {"manager": "none"},
|
|
192
|
+
"paths": {"data": str(data), "logs": []},
|
|
193
|
+
"installedAt": fs.now_iso(), "installedBy": "sample_service.py register",
|
|
194
|
+
}
|
|
195
|
+
try:
|
|
196
|
+
path = fs.write_descriptor(descriptor, Path(args.services_dir) if args.services_dir else None)
|
|
197
|
+
except fs.ServiceError as exc:
|
|
198
|
+
print(str(exc), file=sys.stderr)
|
|
199
|
+
return 1
|
|
200
|
+
print(path)
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def main(argv: Optional[list] = None) -> int:
|
|
205
|
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
206
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
207
|
+
for name in ("serve", "register"):
|
|
208
|
+
p = sub.add_parser(name)
|
|
209
|
+
p.add_argument("--port", type=int, required=True)
|
|
210
|
+
p.add_argument("--data-dir", required=True)
|
|
211
|
+
p.add_argument("--token-file")
|
|
212
|
+
p.add_argument("--id", default="sample")
|
|
213
|
+
p.add_argument("--instance", default="default")
|
|
214
|
+
p.add_argument("--name", default="Sample Service")
|
|
215
|
+
if name == "serve":
|
|
216
|
+
p.add_argument("--degraded", help="report one degraded source with this reason")
|
|
217
|
+
else:
|
|
218
|
+
p.add_argument("--services-dir")
|
|
219
|
+
p.add_argument("--label")
|
|
220
|
+
p.add_argument("--plist")
|
|
221
|
+
args = parser.parse_args(argv)
|
|
222
|
+
return serve(args) if args.command == "serve" else register(args)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
raise SystemExit(main())
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: creating-fabric-agents
|
|
3
3
|
description: Use when designing and building a NEW agent or provider that must be Fabric-compatible from its first commit — «создай агента, совместимого с фабрикой Passion Code», «новый агент под Fabric», "create a fabric-compatible agent", "build a new Fabric provider", "fabric-ready agent from scratch". Runs the intake grill (capability, named consumer, workflow-or-agent, MCP/A2A/local-runner profile, effect declarations), distils source projects into a knowledge pack whose recorded failures become planted eval fixtures, scaffolds the pinned contract bundle, and sets the two-clock eval expectation with the conformance report. NOT for adapting an existing project (use adapting-projects-to-fabric), building the Fabric host or orchestrator, or generic agent design where Fabric compatibility is not part of the request.
|
|
4
|
-
license:
|
|
4
|
+
license: PolyForm-Noncommercial-1.0.0 OR LicenseRef-PolyForm-Internal-Use-1.0.0
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned private fabric-agent-contract checkout. Works without those tools in an explicitly degraded structural-check mode. Ships in one plugin with adapting-projects-to-fabric, whose scripts it reuses.
|
|
6
6
|
metadata:
|
|
7
|
-
author:
|
|
8
|
-
version: "0.3
|
|
7
|
+
author: PassionCode.ai
|
|
8
|
+
version: "0.4.3"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
|
-
contract-commit: "
|
|
10
|
+
contract-commit: "a5a27092ba0dcc5facfbeae8b359146dfb403e9a"
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Creating Fabric-compatible agents
|
|
@@ -30,6 +30,17 @@ Use this skill to design and build a new provider. Do not use it to:
|
|
|
30
30
|
- claim admission: the deliverable is an admission-ready provider bundle, never a
|
|
31
31
|
connected provider.
|
|
32
32
|
|
|
33
|
+
## Handing off to neighbouring skills
|
|
34
|
+
|
|
35
|
+
- The agent's own loop — tool calls, routing and fallback, memory, sub-agents — is
|
|
36
|
+
designed with `agent-orchestrator` (agent-stack).
|
|
37
|
+
- Its eval suite beyond the step-2 observables — trajectories, judges, regression
|
|
38
|
+
fixtures from traces — is built with `agent-evals` (agent-stack).
|
|
39
|
+
- Packaging the agent as an Agent Skill or Claude Code plugin goes through `make-skill`.
|
|
40
|
+
|
|
41
|
+
When one of them is not installed, do that part by hand against its own documentation
|
|
42
|
+
and say in the report which skill was missing.
|
|
43
|
+
|
|
33
44
|
## Step 0 — the intake grill
|
|
34
45
|
|
|
35
46
|
No file is created until every row has an answer. An answer of "later" fails the grill.
|
|
@@ -82,7 +93,7 @@ python3 <plugin-dir>/skills/adapting-projects-to-fabric/scripts/adapt_project.py
|
|
|
82
93
|
--schema-base <immutable-base-uri>
|
|
83
94
|
```
|
|
84
95
|
|
|
85
|
-
Pin exactly contract `0.1.0` at commit `
|
|
96
|
+
Pin exactly contract `0.1.0` at commit `a5a27092ba0dcc5facfbeae8b359146dfb403e9a` and
|
|
86
97
|
read the pinned guide before implementing protocol details. If this skill is installed
|
|
87
98
|
without its sibling, the scaffolder is absent: create the bundle by hand from the pinned
|
|
88
99
|
contract's `docs/guides/connecting-compatible-agents.md` and mark the structural check
|
|
@@ -93,6 +104,13 @@ ready for admission.
|
|
|
93
104
|
|
|
94
105
|
## Step 4 — implement against the observables
|
|
95
106
|
|
|
107
|
+
If the agent keeps running on the operator's computer — it answers other agents, runs
|
|
108
|
+
long jobs or shows a dashboard — build it as a service with the sibling skill
|
|
109
|
+
`building-fabric-services`: its instance lock, launchd plist, descriptor, well-known
|
|
110
|
+
document and events feed are part of this step, not a later retrofit. If that skill is
|
|
111
|
+
absent, apply the `fabric-service/0.1` rules from the pinned contract's
|
|
112
|
+
`docs/specification/service.md` by hand and mark its probe `NOT_RUN`.
|
|
113
|
+
|
|
96
114
|
Build the capability behind the chosen surface. Keep model choice and internal reasoning
|
|
97
115
|
outside the contract; expose typed outcomes, evidence, and protocol-visible state. Every
|
|
98
116
|
provider output is untrusted until schemas and semantic assertions pass. The step-2
|