@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.
Files changed (21) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/LICENSE +151 -17
  3. package/README.md +51 -6
  4. package/SECURITY.md +16 -3
  5. package/bin/fabric-agent-adapter.js +87 -43
  6. package/package.json +13 -8
  7. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +5 -6
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +8 -5
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +1 -1
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +240 -0
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +32 -0
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/events-and-notifications.md +44 -0
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +63 -0
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +32 -0
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +83 -0
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +58 -0
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +373 -0
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +380 -0
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +663 -0
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +226 -0
  21. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +23 -5
@@ -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: MIT
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: passioncode-ai
8
- version: "0.3.1"
7
+ author: PassionCode.ai
8
+ version: "0.4.3"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "20a818e648a4c09a60df0126d11626922e8b9094"
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 `20a818e648a4c09a60df0126d11626922e8b9094` and
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